Minetest-WorldEditAdditions/worldeditadditions/utils/terrain.lua

179 lines
8.2 KiB
Lua

local wea = worldeditadditions
--- 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 = {
z = (pos2.z - pos1.z) + 1,
x = (pos2.x - pos1.x) + 1
}
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