Refactor: Split up terrain.lua, make subtable wea.terrain

This is just the start, if my plans work out.

The eventual aim here is to implement a generic Heightmap2D class, just 
like Vector3.

This will make interacting with heightmaps much easier.
This commit is contained in:
Starbeamrainbowlabs 2021-12-31 02:42:32 +00:00
parent 9df5ba6fe5
commit 1310dae884
No known key found for this signature in database
GPG Key ID: 1BE5172E637709C2
14 changed files with 271 additions and 237 deletions

@ -23,6 +23,7 @@ wea.io = dofile(wea.modpath.."/utils/io.lua")
wea.bit = dofile(wea.modpath.."/utils/bit.lua") wea.bit = dofile(wea.modpath.."/utils/bit.lua")
wea.terrain = dofile(wea.modpath.."/utils/terrain.lua")
dofile(wea.modpath.."/utils/vector.lua") dofile(wea.modpath.."/utils/vector.lua")
dofile(wea.modpath.."/utils/strings/init.lua") dofile(wea.modpath.."/utils/strings/init.lua")
@ -33,7 +34,7 @@ dofile(wea.modpath.."/utils/tables/init.lua")
dofile(wea.modpath.."/utils/numbers.lua") dofile(wea.modpath.."/utils/numbers.lua")
dofile(wea.modpath.."/utils/nodes.lua") dofile(wea.modpath.."/utils/nodes.lua")
dofile(wea.modpath.."/utils/node_identification.lua") dofile(wea.modpath.."/utils/node_identification.lua")
dofile(wea.modpath.."/utils/terrain.lua")
dofile(wea.modpath.."/utils/raycast_adv.lua") -- For the farwand dofile(wea.modpath.."/utils/raycast_adv.lua") -- For the farwand
dofile(wea.modpath.."/utils/player.lua") -- Player info functions dofile(wea.modpath.."/utils/player.lua") -- Player info functions

@ -1,11 +1,12 @@
local Vector3 = worldeditadditions.Vector3 local wea = worldeditadditions
local Vector3 = wea.Vector3
worldeditadditions.conv = {} wea.conv = {}
dofile(worldeditadditions.modpath.."/lib/conv/kernels.lua") dofile(wea.modpath.."/lib/conv/kernels.lua")
dofile(worldeditadditions.modpath.."/lib/conv/kernel_gaussian.lua") dofile(wea.modpath.."/lib/conv/kernel_gaussian.lua")
dofile(worldeditadditions.modpath.."/lib/conv/convolve.lua") dofile(wea.modpath.."/lib/conv/convolve.lua")
--- Creates a new kernel. --- Creates a new kernel.
-- Note that the gaussian kernel only allows for creating a square kernel. -- Note that the gaussian kernel only allows for creating a square kernel.
@ -14,7 +15,7 @@ dofile(worldeditadditions.modpath.."/lib/conv/convolve.lua")
-- @param width number The width of the kernel to create (must be an odd integer). -- @param width number The width of the kernel to create (must be an odd integer).
-- @param height number The height of the kernel to create (must be an odd integer). -- @param height number The height of the kernel to create (must be an odd integer).
-- @param arg number The argument to pass when creating the kernel. Currently only used by gaussian kernel as the sigma value. -- @param arg number The argument to pass when creating the kernel. Currently only used by gaussian kernel as the sigma value.
function worldeditadditions.get_conv_kernel(name, width, height, arg) function wea.get_conv_kernel(name, width, height, arg)
if width % 2 ~= 1 then if width % 2 ~= 1 then
return false, "Error: The width must be an odd integer."; return false, "Error: The width must be an odd integer.";
end end
@ -23,16 +24,16 @@ function worldeditadditions.get_conv_kernel(name, width, height, arg)
end end
if name == "box" then if name == "box" then
return true, worldeditadditions.conv.kernel_box(width, height) return true, wea.conv.kernel_box(width, height)
elseif name == "pascal" then elseif name == "pascal" then
return true, worldeditadditions.conv.kernel_pascal(width, height, true) return true, wea.conv.kernel_pascal(width, height, true)
elseif name == "gaussian" then elseif name == "gaussian" then
if width ~= height then if width ~= height then
return false, "Error: When using a gaussian kernel the width and height must be identical." return false, "Error: When using a gaussian kernel the width and height must be identical."
end end
-- Default to sigma = 2 -- Default to sigma = 2
if arg == nil then arg = 2 end if arg == nil then arg = 2 end
local success, result = worldeditadditions.conv.kernel_gaussian(width, arg) local success, result = wea.conv.kernel_gaussian(width, arg)
return success, result return success, result
end end
@ -40,7 +41,7 @@ function worldeditadditions.get_conv_kernel(name, width, height, arg)
end end
function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size) function wea.convolve(pos1, pos2, kernel, kernel_size)
pos1, pos2 = worldedit.sort_pos(pos1, pos2) pos1, pos2 = worldedit.sort_pos(pos1, pos2)
local border_size = Vector3.new( local border_size = Vector3.new(
@ -61,10 +62,10 @@ function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size)
local node_id_air = minetest.get_content_id("air") local node_id_air = minetest.get_content_id("air")
local heightmap, heightmap_size = worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) local heightmap, heightmap_size = wea.terrain.make_heightmap(pos1, pos2, manip, area, data)
local heightmap_conv = worldeditadditions.table.shallowcopy(heightmap) local heightmap_conv = wea.table.shallowcopy(heightmap)
worldeditadditions.conv.convolve( wea.conv.convolve(
heightmap_conv, heightmap_conv,
heightmap_size, heightmap_size,
kernel, kernel,
@ -72,11 +73,11 @@ function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size)
) )
-- print("original") -- print("original")
-- worldeditadditions.format.array_2d(heightmap, (pos2.z - pos1.z) + 1) -- wea.format.array_2d(heightmap, (pos2.z - pos1.z) + 1)
-- print("transformed") -- print("transformed")
-- worldeditadditions.format.array_2d(heightmap_conv, (pos2.z - pos1.z) + 1) -- wea.format.array_2d(heightmap_conv, (pos2.z - pos1.z) + 1)
worldeditadditions.apply_heightmap_changes( wea.terrain.apply_heightmap_changes(
pos1, pos2, area, data, pos1, pos2, area, data,
heightmap, heightmap_conv, heightmap_size heightmap, heightmap_conv, heightmap_size
) )

@ -1,10 +1,11 @@
worldeditadditions.erode = {} local wea = worldeditadditions
wea.erode = {}
dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua") dofile(wea.modpath.."/lib/erode/snowballs.lua")
dofile(worldeditadditions.modpath.."/lib/erode/river.lua") dofile(wea.modpath.."/lib/erode/river.lua")
function worldeditadditions.erode.run(pos1, pos2, algorithm, params) function wea.erode.run(pos1, pos2, algorithm, params)
pos1, pos2 = worldedit.sort_pos(pos1, pos2) pos1, pos2 = worldedit.sort_pos(pos1, pos2)
local manip, area = worldedit.manip_helpers.init(pos1, pos2) local manip, area = worldedit.manip_helpers.init(pos1, pos2)
@ -17,15 +18,15 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
local region_height = (pos2.y - pos1.y) + 1 local region_height = (pos2.y - pos1.y) + 1
local heightmap = worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) local heightmap = wea.terrain.make_heightmap(pos1, pos2, manip, area, data)
local heightmap_eroded = worldeditadditions.table.shallowcopy(heightmap) local heightmap_eroded = wea.table.shallowcopy(heightmap)
-- print("[erode.run] algorithm: "..algorithm..", params:"); -- print("[erode.run] algorithm: "..algorithm..", params:");
-- print(worldeditadditions.format.map(params)) -- print(wea.format.map(params))
-- worldeditadditions.format.array_2d(heightmap, heightmap_size.x) -- wea.format.array_2d(heightmap, heightmap_size.x)
local success, msg, stats local success, msg, stats
if algorithm == "snowballs" then if algorithm == "snowballs" then
success, msg = worldeditadditions.erode.snowballs( success, msg = wea.erode.snowballs(
heightmap, heightmap_eroded, heightmap, heightmap_eroded,
heightmap_size, heightmap_size,
region_height, region_height,
@ -33,7 +34,7 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
) )
if not success then return success, msg end if not success then return success, msg end
elseif algorithm == "river" then elseif algorithm == "river" then
success, msg = worldeditadditions.erode.river( success, msg = wea.erode.river(
heightmap, heightmap_eroded, heightmap, heightmap_eroded,
heightmap_size, heightmap_size,
region_height, region_height,
@ -48,7 +49,7 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like), river (2d; cellular automata-like; fills potholes and lowers towers). Ideas for algorithms to implement are welcome!" return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like), river (2d; cellular automata-like; fills potholes and lowers towers). Ideas for algorithms to implement are welcome!"
end end
success, stats = worldeditadditions.apply_heightmap_changes( success, stats = wea.terrain.apply_heightmap_changes(
pos1, pos2, area, data, pos1, pos2, area, data,
heightmap, heightmap_eroded, heightmap_size heightmap, heightmap_eroded, heightmap_size
) )

@ -1,4 +1,5 @@
local Vector3 = worldeditadditions.Vector3 local wea = worldeditadditions
local Vector3 = wea.Vector3
-- Test command: //multi //fp set1 1313 6 5540 //fp set2 1338 17 5521 //erode snowballs -- Test command: //multi //fp set1 1313 6 5540 //fp set2 1338 17 5521 //erode snowballs
@ -28,7 +29,7 @@ local function snowball(heightmap, normalmap, heightmap_size, startpos, params)
end end
if #hist_velocity > 0 and i > 5 if #hist_velocity > 0 and i > 5
and worldeditadditions.average(hist_velocity) < 0.03 then and wea.average(hist_velocity) < 0.03 then
-- print("[snowball] It looks like we've stopped") -- print("[snowball] It looks like we've stopped")
return true, i return true, i
end end
@ -51,11 +52,11 @@ local function snowball(heightmap, normalmap, heightmap_size, startpos, params)
velocity.x = params.friction * velocity.x + normalmap[hi].x * params.speed velocity.x = params.friction * velocity.x + normalmap[hi].x * params.speed
velocity.z = params.friction * velocity.z + normalmap[hi].y * 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) -- print("[snowball] now at ("..x..", "..z..") velocity "..wea.vector.lengthsquared(velocity)..", sediment "..sediment)
local new_vel_sq = worldeditadditions.vector.lengthsquared(velocity) local new_vel_sq = wea.vector.lengthsquared(velocity)
if new_vel_sq > 1 then if new_vel_sq > 1 then
-- print("[snowball] velocity squared over 1, normalising") -- print("[snowball] velocity squared over 1, normalising")
velocity = worldeditadditions.vector.normalize(velocity) velocity = wea.vector.normalize(velocity)
end end
table.insert(hist_velocity, new_vel_sq) table.insert(hist_velocity, new_vel_sq)
if #hist_velocity > params.velocity_hist_count then table.remove(hist_velocity, 1) end if #hist_velocity > params.velocity_hist_count then table.remove(hist_velocity, 1) end
@ -74,7 +75,7 @@ Note that this *mutates* the given heightmap.
@source https://jobtalle.com/simulating_hydraulic_erosion.html @source https://jobtalle.com/simulating_hydraulic_erosion.html
]]-- ]]--
function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, heightmap_size, region_height, params_custom) function wea.erode.snowballs(heightmap_initial, heightmap, heightmap_size, region_height, params_custom)
local params = { local params = {
rate_deposit = 0.03, -- 0.03 rate_deposit = 0.03, -- 0.03
rate_erosion = 0.04, -- 0.04 rate_erosion = 0.04, -- 0.04
@ -88,12 +89,12 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
count = 25000 count = 25000
} }
-- Apply the custom settings -- Apply the custom settings
worldeditadditions.table.apply(params_custom, params) wea.table.apply(params_custom, params)
-- print("[erode/snowballs] params: ") -- print("[erode/snowballs] params: ")
-- print(worldeditadditions.format.map(params)) -- print(wea.format.map(params))
local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) local normals = wea.terrain.calculate_normals(heightmap, heightmap_size)
local stats_steps = {} local stats_steps = {}
for i = 1, params.count do for i = 1, params.count do
@ -111,7 +112,7 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
if not success then return false, "Error: Failed at snowball "..i..":"..steps end if not success then return false, "Error: Failed at snowball "..i..":"..steps end
end end
-- print("[snowballs] "..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps, averaged ~"..worldeditadditions.average(stats_steps).."") -- print("[snowballs] "..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps, averaged ~"..wea.average(stats_steps).."")
-- Round everything to the nearest int, since you can't really have -- Round everything to the nearest int, since you can't really have
-- something like .141592671 of a node -- something like .141592671 of a node
@ -131,15 +132,15 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
end end
if not params.noconv then if not params.noconv then
local success, matrix = worldeditadditions.get_conv_kernel("gaussian", 3, 3) local success, matrix = wea.get_conv_kernel("gaussian", 3, 3)
if not success then return success, matrix end if not success then return success, matrix end
local matrix_size = Vector3.new(3, 0, 3) local matrix_size = Vector3.new(3, 0, 3)
worldeditadditions.conv.convolve( wea.conv.convolve(
heightmap, heightmap_size, heightmap, heightmap_size,
matrix, matrix,
matrix_size matrix_size
) )
end end
return true, ""..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps (averaged ~"..worldeditadditions.average(stats_steps).." steps)" return true, ""..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps (averaged ~"..wea.average(stats_steps).." steps)"
end end

@ -35,11 +35,11 @@ function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slop
local node_ids, node_ids_count = wea.unwind_node_list(node_weights) local node_ids, node_ids_count = wea.unwind_node_list(node_weights)
local heightmap, heightmap_size = wea.make_heightmap( local heightmap, heightmap_size = wea.terrain.make_heightmap(
pos1, pos2, pos1, pos2,
manip, area, data manip, area, data
) )
local slopemap = wea.calculate_slopes(heightmap, heightmap_size) local slopemap = wea.terrain.calculate_slopes(heightmap, heightmap_size)
-- worldeditadditions.format.array_2d(heightmap, heightmap_size.x) -- worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
-- print_slopes(slopemap, heightmap_size.x) -- print_slopes(slopemap, heightmap_size.x)
--luacheck:ignore 311 --luacheck:ignore 311

@ -3,13 +3,13 @@ local wea = worldeditadditions
--- Applies the given noise field to the given heightmap. --- Applies the given noise field to the given heightmap.
-- Mutates the given heightmap. -- Mutates the given heightmap.
-- @param heightmap number[] A table of ZERO indexed numbers representing the heghtmap - see worldeditadditions.make_heightmap(). -- @param heightmap number[] A table of ZERO indexed numbers representing the heghtmap - see worldeditadditions.terrain.make_heightmap().
-- @param noise number[] An table identical in structure to the heightmap containing the noise values to apply. -- @param noise number[] An table identical in structure to the heightmap containing the noise values to apply.
-- @param heightmap_size {x:number,z:number} A 2d vector representing the size of the heightmap. -- @param heightmap_size {x:number,z:number} A 2d vector representing the size of the heightmap.
-- @param region_height number The height of the defined region. -- @param region_height number The height of the defined region.
-- @param apply_mode string The apply mode to use to apply the noise to the heightmap. -- @param apply_mode string The apply mode to use to apply the noise to the heightmap.
-- @returns bool[,string] A boolean value representing whether the application was successful or not. If false, then an error message as a string is also returned describing the error that occurred. -- @returns bool[,string] A boolean value representing whether the application was successful or not. If false, then an error message as a string is also returned describing the error that occurred.
function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, pos1, pos2, apply_mode) function wea.noise.apply_2d(heightmap, noise, heightmap_size, pos1, pos2, apply_mode)
if type(apply_mode) ~= "string" and type(apply_mode) ~= "number" then if type(apply_mode) ~= "string" and type(apply_mode) ~= "number" then
return false, "Error: Expected value of type string or number for apply_mode, but received value of type "..type(apply_mode) return false, "Error: Expected value of type string or number for apply_mode, but received value of type "..type(apply_mode)
end end
@ -17,12 +17,12 @@ function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, pos
local region_height = pos2.y - pos1.y local region_height = pos2.y - pos1.y
print("NOISE APPLY_2D\n") -- print("NOISE APPLY_2D\n")
worldeditadditions.format.array_2d(noise, heightmap_size.x) wea.format.array_2d(noise, heightmap_size.x)
local height = tonumber(apply_mode) local height = tonumber(apply_mode)
print("DEBUG apply_mode", apply_mode, "as height", height) -- print("DEBUG apply_mode", apply_mode, "as height", height)
for z = heightmap_size.z - 1, 0, -1 do for z = heightmap_size.z - 1, 0, -1 do
for x = heightmap_size.x - 1, 0, -1 do for x = heightmap_size.x - 1, 0, -1 do
@ -53,8 +53,8 @@ function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, pos
-- heightmap[(z * heightmap_size.x) + x] = z -- heightmap[(z * heightmap_size.x) + x] = z
-- end -- end
print("HEIGHTMAP\n") -- print("HEIGHTMAP\n")
worldeditadditions.format.array_2d(heightmap, heightmap_size.x) -- worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
return true return true

@ -12,25 +12,25 @@ local wea = worldeditadditions
-- @param pos1 Vector pos1 of the defined region -- @param pos1 Vector pos1 of the defined region
-- @param pos2 Vector pos2 of the defined region -- @param pos2 Vector pos2 of the defined region
-- @param noise_params table A noise parameters table. -- @param noise_params table A noise parameters table.
function worldeditadditions.noise.run2d(pos1, pos2, noise_params) function wea.noise.run2d(pos1, pos2, noise_params)
pos1, pos2 = worldedit.sort_pos(pos1, pos2) pos1, pos2 = worldedit.sort_pos(pos1, pos2)
-- pos2 will always have the highest co-ordinates now -- pos2 will always have the highest co-ordinates now
-- Fill in the default params -- Fill in the default params
-- print("DEBUG noise_params_custom ", wea.format.map(noise_params)) -- print("DEBUG noise_params_custom ", wea.format.map(noise_params))
noise_params = worldeditadditions.noise.params_apply_default(noise_params) noise_params = wea.noise.params_apply_default(noise_params)
-- print("DEBUG noise_params[1] ", wea.format.map(noise_params[1])) -- print("DEBUG noise_params[1] ", wea.format.map(noise_params[1]))
-- Fetch the nodes in the specified area -- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2) local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data() local data = manip:get_data()
local heightmap_old, heightmap_size = worldeditadditions.make_heightmap( local heightmap_old, heightmap_size = wea.terrain.make_heightmap(
pos1, pos2, pos1, pos2,
manip, area, manip, area,
data data
) )
local heightmap_new = worldeditadditions.table.shallowcopy(heightmap_old) local heightmap_new = wea.table.shallowcopy(heightmap_old)
local success, noisemap = wea.noise.make_2d( local success, noisemap = wea.noise.make_2d(
heightmap_size, heightmap_size,
@ -49,7 +49,7 @@ function worldeditadditions.noise.run2d(pos1, pos2, noise_params)
if not success then return success, message end if not success then return success, message end
local stats local stats
success, stats = wea.apply_heightmap_changes( success, stats = wea.terrain.apply_heightmap_changes(
pos1, pos2, pos1, pos2,
area, data, area, data,
heightmap_old, heightmap_new, heightmap_old, heightmap_new,

@ -36,7 +36,7 @@ local function apply(pos1, brush_name, height, brush_size)
local manip, area = worldedit.manip_helpers.init(pos1_compute, pos2_compute) local manip, area = worldedit.manip_helpers.init(pos1_compute, pos2_compute)
local data = manip:get_data() local data = manip:get_data()
local heightmap, heightmap_size = wea.make_heightmap( local heightmap, heightmap_size = wea.terrain.make_heightmap(
pos1_compute, pos2_compute, pos1_compute, pos2_compute,
manip, area, manip, area,
data data
@ -52,7 +52,7 @@ local function apply(pos1, brush_name, height, brush_size)
if not success2 then return success2, added end if not success2 then return success2, added end
-- 3: Save back to disk & return -- 3: Save back to disk & return
local success3, stats = wea.apply_heightmap_changes( local success3, stats = wea.terrain.apply_heightmap_changes(
pos1_compute, pos2_compute, pos1_compute, pos2_compute,
area, data, area, data,
heightmap_orig, heightmap, heightmap_orig, heightmap,

@ -1,180 +0,0 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- 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,table The ZERO-indexed heightmap data (as 1 single flat array), followed by the size of the heightmap in the form { z = size_z, x = size_x }.
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 (wea.is_airlike(data[i]) or wea.is_liquidlike(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
local heightmap_size = Vector3.new(
(pos2.x - pos1.x) + 1, -- x
0, -- y
(pos2.z - pos1.z) + 1 -- z
)
return heightmap, heightmap_size
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 Vector3 instance representing a normal.
function worldeditadditions.calculate_normals(heightmap, heightmap_size)
-- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z)
local result = {}
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-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.x + 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.x + x] end
if z + 1 < heightmap_size.z-1 then down = heightmap[(z+1)*heightmap_size.x + x] end
if x - 1 > 0 then left = heightmap[z*heightmap_size.x + (x-1)] end
if x + 1 < heightmap_size.x-1 then right = heightmap[z*heightmap_size.x + (x+1)] end
-- print("[normals] UP | index", (z-1)*heightmap_size.x + x, "z", z, "z-1", z - 1, "up", up, "limit", 0)
-- print("[normals] DOWN | index", (z+1)*heightmap_size.x + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size.x-1)
-- print("[normals] LEFT | index", z*heightmap_size.x + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0)
-- print("[normals] RIGHT | index", z*heightmap_size.x + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size.x-1)
result[hi] = wea.Vector3.new(
left - right, -- x
2, -- y - Z & Y are flipped
down - up -- z
):normalise()
-- print("[normals] at "..hi.." ("..x..", "..z..") normal "..result[hi])
end
end
return result
end
--- Converts a 2d heightmap into slope values in radians.
-- Convert a radians to degrees by doing (radians*math.pi) / 180 for display,
-- but it is STRONGLY recommended to keep all internal calculations in radians.
-- @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 slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians.
function worldeditadditions.calculate_slopes(heightmap, heightmap_size)
local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size)
local slopes = { }
local up = wea.Vector3.new(0, 1, 0) -- Z & Y are flipped
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-1, 0, -1 do
local hi = z*heightmap_size.x + x
-- Ref https://stackoverflow.com/a/16669463/1460422
-- slopes[hi] = wea.Vector3.dot_product(normals[hi], up)
slopes[hi] = math.acos(normals[hi].y)
end
end
return slopes
end
--- Applies changes to a heightmap to a Voxel Manipulator data block.
-- @param pos1 vector Position 1 of the defined region
-- @param pos2 vector Position 2 of the defined region
-- @param area VoxelArea The VoxelArea object (see worldedit.manip_helpers.init)
-- @param data number[] The node ids data array containing the slice of the Minetest world extracted using the Voxel Manipulator.
-- @param heightmap_old number[] The original heightmap from worldeditadditions.make_heightmap.
-- @param heightmap_new number[] The new heightmap containing the altered updated values. It is expected that worldeditadditions.table.shallowcopy be used to make a COPY of the data worldeditadditions.make_heightmap for this purpose. Both heightmap_old AND heightmap_new are REQUIRED in order for this function to work.
-- @param heightmap_size vector The x / z size of the heightmap. Any y value set in the vector is ignored.
-- @returns bool, string|{ added: number, removed: number } A bool indicating whether the operation was successful or not, followed by either an error message as a string (if it was not successful) or a table of statistics (if it was successful).
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")
local node_id_ignore = minetest.get_content_id("ignore")
for z = heightmap_size.z - 1, 0, -1 do
for x = heightmap_size.x - 1, 0, -1 do
local hi = z*heightmap_size.x + x
local height_old = heightmap_old[hi]
local height_new = heightmap_new[hi]
-- 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
-- noop
elseif height_new < height_old then
local node_id_replace = data[area:index(
pos1.x + x,
pos1.y + height_old + 1,
pos1.z + z
)]
-- Unlikely, but if it can happen, it *will* happen.....
if node_id_replace == node_id_ignore then
node_id_replace = node_id_air
end
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]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id_replace
end
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]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id
end
y = y + 1
end
end
end
end
return true, stats
end

@ -0,0 +1,71 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Applies changes to a heightmap to a Voxel Manipulator data block.
-- @param pos1 vector Position 1 of the defined region
-- @param pos2 vector Position 2 of the defined region
-- @param area VoxelArea The VoxelArea object (see worldedit.manip_helpers.init)
-- @param data number[] The node ids data array containing the slice of the Minetest world extracted using the Voxel Manipulator.
-- @param heightmap_old number[] The original heightmap from worldeditadditions.make_heightmap.
-- @param heightmap_new number[] The new heightmap containing the altered updated values. It is expected that worldeditadditions.table.shallowcopy be used to make a COPY of the data worldeditadditions.make_heightmap for this purpose. Both heightmap_old AND heightmap_new are REQUIRED in order for this function to work.
-- @param heightmap_size vector The x / z size of the heightmap. Any y value set in the vector is ignored.
-- @returns bool, string|{ added: number, removed: number } A bool indicating whether the operation was successful or not, followed by either an error message as a string (if it was not successful) or a table of statistics (if it was successful).
local function 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")
local node_id_ignore = minetest.get_content_id("ignore")
for z = heightmap_size.z - 1, 0, -1 do
for x = heightmap_size.x - 1, 0, -1 do
local hi = z*heightmap_size.x + x
local height_old = heightmap_old[hi]
local height_new = heightmap_new[hi]
-- 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
-- noop
elseif height_new < height_old then
local node_id_replace = data[area:index(
pos1.x + x,
pos1.y + height_old + 1,
pos1.z + z
)]
-- Unlikely, but if it can happen, it *will* happen.....
if node_id_replace == node_id_ignore then
node_id_replace = node_id_air
end
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]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id_replace
end
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]))
if data[ci] ~= node_id_ignore then
data[ci] = node_id
end
y = y + 1
end
end
end
end
return true, stats
end
return apply_heightmap_changes

@ -0,0 +1,47 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- 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.terrain.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 Vector3 instance representing a normal.
local function calculate_normals(heightmap, heightmap_size)
-- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z)
local result = {}
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-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.x + 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.x + x] end
if z + 1 < heightmap_size.z-1 then down = heightmap[(z+1)*heightmap_size.x + x] end
if x - 1 > 0 then left = heightmap[z*heightmap_size.x + (x-1)] end
if x + 1 < heightmap_size.x-1 then right = heightmap[z*heightmap_size.x + (x+1)] end
-- print("[normals] UP | index", (z-1)*heightmap_size.x + x, "z", z, "z-1", z - 1, "up", up, "limit", 0)
-- print("[normals] DOWN | index", (z+1)*heightmap_size.x + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size.x-1)
-- print("[normals] LEFT | index", z*heightmap_size.x + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0)
-- print("[normals] RIGHT | index", z*heightmap_size.x + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size.x-1)
result[hi] = Vector3.new(
left - right, -- x
2, -- y - Z & Y are flipped
down - up -- z
):normalise()
-- print("[normals] at "..hi.." ("..x..", "..z..") normal "..result[hi])
end
end
return result
end
return calculate_normals

@ -0,0 +1,30 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Converts a 2d heightmap into slope values in radians.
-- Convert a radians to degrees by doing (radians*math.pi) / 180 for display,
-- but it is STRONGLY recommended to keep all internal calculations in radians.
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.terrain.make_heightmap().
-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ]
-- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians.
local function calculate_slopes(heightmap, heightmap_size)
local normals = wea.terrain.calculate_normals(heightmap, heightmap_size)
local slopes = { }
local up = wea.Vector3.new(0, 1, 0) -- Z & Y are flipped
for z = heightmap_size.z-1, 0, -1 do
for x = heightmap_size.x-1, 0, -1 do
local hi = z*heightmap_size.x + x
-- Ref https://stackoverflow.com/a/16669463/1460422
-- slopes[hi] = wea.Vector3.dot_product(normals[hi], up)
slopes[hi] = math.acos(normals[hi].y)
end
end
return slopes
end
return calculate_slopes

@ -0,0 +1,12 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local terrain = {
make_heightmap = dofile(wea.modpath.."/utils/terrain/make_heightmap.lua"),
calculate_normals = dofile(wea.modpath.."/utils/terrain/calculate_normals.lua"),
calculate_slopes = dofile(wea.modpath.."/utils/terrain/calculate_slopes.lua"),
apply_heightmap_changes = dofile(wea.modpath.."/utils/terrain/apply_heightmap_changes.lua")
}
return terrain

@ -0,0 +1,50 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- 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,table The ZERO-indexed heightmap data (as 1 single flat array), followed by the size of the heightmap in the form { z = size_z, x = size_x }.
local function 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 (wea.is_airlike(data[i]) or wea.is_liquidlike(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
local heightmap_size = Vector3.new(
(pos2.x - pos1.x) + 1, -- x
0, -- y
(pos2.z - pos1.z) + 1 -- z
)
return heightmap, heightmap_size
end
return make_heightmap