Implement apply function for VoxelManipulators

This commit is contained in:
Starbeamrainbowlabs 2021-12-28 02:17:26 +00:00
parent 75ffa81b7e
commit b0c3d34dd0
Signed by: sbrl
GPG Key ID: 1BE5172E637709C2
4 changed files with 184 additions and 40 deletions

View File

@ -1,53 +1,65 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Applies the given brush at the given x/z position to the given heightmap.
-- Important: Where a Vector3 is mentioned in the parameter list, it reall MUST
-- be a Vector3 instance.
-- Also important: Remember that the position there is RELATIVE TO THE HEIGHTMAP'S origin (0, 0) and is on the X and Z axes!
-- @param brush table The ZERO-indexed brush to apply. Values should be normalised to be between 0 and 1.
-- @param brush_size Vector3 The size of the brush on the x/y axes.
-- @pram height number The multiplier to apply to each brush pixel value just before applying it. Negative values are allowed - this will cause a subtraction operation instead of an addition.
-- @param position Vector3 The position RELATIVE TO THE HEIGHTMAP on the x/z coordinates to centre the brush application on.
-- @param heightmap table The heightmap to apply the brush to. See worldeditadditions.make_heightmap for how to obtain one of these.
-- @param heightmap_size Vector3 The size of the aforementioned heightmap. See worldeditadditions.make_heightmap for more information.
-- @returns true,number,number|false,string If the operation was not successful, then false followed by an error message as a string is returned. If it was successful however, 3 values are returned: true, then the number of nodes added, then the number of nodes removed.
local function apply(brush, brush_size, height, position, heightmap, heightmap_size)
-- Convert brush_size to match the scheme used in the heightmap
brush_size = brush_size:clone()
brush_size.z = brush_size.y
brush_size.y = 0
--- Applies the given brush with the given height and size to the given position.
-- @param pos1 Vector3 The position at which to apply the brush.
-- @param brush_name string The name of the brush to apply.
-- @param height number The height of the brush application.
-- @param brush_size Vector3 The size of the brush application. Values are interpreted on the X/Y coordinates, and NOT X/Z!
-- @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(pos1, brush_name, height, brush_size)
-- 1: Get & validate brush
local success, brush, brush_size_actual = wea.sculpt.make_brush(brush_name, brush_size)
if not success then return success, brush end
local brush_radius = (brush_size/2):ceil() - 1
local pos_start = (position - brush_radius)
:clamp(Vector3.new(0, 0, 0), heightmap_size)
local pos_end = (pos_start + brush_size)
:clamp(Vector3.new(0, 0, 0), heightmap_size)
local brush_size_terrain = Vector3.new(
brush_size_actual.x,
0,
brush_size_actual.y
)
local brush_size_radius = (brush_size_terrain / 2):floor()
local added = 0
local removed = 0
local pos1_compute = pos1 - brush_size_radius
local pos2_compute = pos1 + brush_size_radius + Vector3.new(0, height, 0)
-- Iterate over the heightmap and apply the brush
-- Note that we do not iterate over the brush, because we don't know if the
-- brush actually fits inside the region.... O.o
for z = pos_end, pos_start, -1 do
for x = pos_end, pos_start, -1 do
local hi = z*heightmap_size.x + x
local pos_brush = Vector3.new(x, 0, z) - pos_start
local bi = pos_brush.z*brush_size.x + pos_brush.x
-- 2: Fetch the nodes in the specified area, extract heightmap
local manip, area = worldedit.manip_helpers.init(pos1_compute, pos2_compute)
local data = manip:get_data()
local heightmap, heightmap_size = wea.make_heightmap(
pos1_compute, pos2_compute,
manip, area,
data
)
local heightmap_orig = wea.table.shallowcopy(heightmap)
for z = pos2_compute.z, pos1_compute.z, -1 do
for x = pos2_compute.x, pos1_compute.x, -1 do
local next_index = 1 -- We use table.insert() in make_weighted
local placed_node = false
local adjustment = math.floor(brush[bi]*height)
if adjustment > 0 then
added = added + adjustment
elseif adjustment < 0 then
removed = removed + math.abs(adjustment)
end
local hi = (z-pos1_compute.z)*heightmap_size.x + (x-pos1_compute.x)
heightmap[hi] = heightmap[hi] + adjustment
local offset = brush[hi] * height
if height > 0 then offset = math.floor(offset)
else offset = math.ceil(offset) end
heightmap[hi] = heightmap[hi] + offset
end
end
return true, added, removed
-- 3: Save back to disk & return
local success2, stats = wea.apply_heightmap_changes(
pos1_compute, pos2_compute,
area, data,
heightmap_orig, heightmap,
heightmap_size
)
if not success2 then return success2, stats end
worldedit.manip_helpers.finish(manip, data)
return true, stats
end

View File

@ -0,0 +1,54 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
--- Applies the given brush at the given x/z position to the given heightmap.
-- Important: Where a Vector3 is mentioned in the parameter list, it reall MUST
-- be a Vector3 instance.
-- Also important: Remember that the position there is RELATIVE TO THE HEIGHTMAP'S origin (0, 0) and is on the X and Z axes!
-- @param brush table The ZERO-indexed brush to apply. Values should be normalised to be between 0 and 1.
-- @param brush_size Vector3 The size of the brush on the x/y axes.
-- @pram height number The multiplier to apply to each brush pixel value just before applying it. Negative values are allowed - this will cause a subtraction operation instead of an addition.
-- @param position Vector3 The position RELATIVE TO THE HEIGHTMAP on the x/z coordinates to centre the brush application on.
-- @param heightmap table The heightmap to apply the brush to. See worldeditadditions.make_heightmap for how to obtain one of these.
-- @param heightmap_size Vector3 The size of the aforementioned heightmap. See worldeditadditions.make_heightmap for more information.
-- @returns true,number,number|false,string If the operation was not successful, then false followed by an error message as a string is returned. If it was successful however, 3 values are returned: true, then the number of nodes added, then the number of nodes removed.
local function apply_heightmap(brush, brush_size, height, position, heightmap, heightmap_size)
-- Convert brush_size to match the scheme used in the heightmap
brush_size = brush_size:clone()
brush_size.z = brush_size.y
brush_size.y = 0
local brush_radius = (brush_size/2):ceil() - 1
local pos_start = (position - brush_radius)
:clamp(Vector3.new(0, 0, 0), heightmap_size)
local pos_end = (pos_start + brush_size)
:clamp(Vector3.new(0, 0, 0), heightmap_size)
local added = 0
local removed = 0
-- Iterate over the heightmap and apply the brush
-- Note that we do not iterate over the brush, because we don't know if the
-- brush actually fits inside the region.... O.o
for z = pos_end, pos_start, -1 do
for x = pos_end, pos_start, -1 do
local hi = z*heightmap_size.x + x
local pos_brush = Vector3.new(x, 0, z) - pos_start
local bi = pos_brush.z*brush_size.x + pos_brush.x
local adjustment = math.floor(brush[bi]*height)
if adjustment > 0 then
added = added + adjustment
elseif adjustment < 0 then
removed = removed + math.abs(adjustment)
end
heightmap[hi] = heightmap[hi] + adjustment
end
end
return true, added, removed
end
return apply_heightmap

View File

@ -10,6 +10,7 @@ local sculpt = {
make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua"),
preview_brush = dofile(wea.modpath.."/lib/sculpt/preview_brush.lua"),
read_brush_static = dofile(wea.modpath.."/lib/sculpt/read_brush_static.lua"),
apply_heightmap = dofile(wea.modpath.."/lib/sculpt/apply_heightmap.lua"),
apply = dofile(wea.modpath.."/lib/sculpt/apply.lua")
}
@ -17,6 +18,6 @@ return sculpt
-- TODO: Automatically find & register all text file based brushes in the brushes directory
-- TODO: Implement automatic scaling of static brushes to the correct size. We have ..scale already, but we probably need to implement a proper 2d canvas scaling algorithm. Some options to consider: linear < [bi]cubic < nohalo/lohalo
-- TODO: Implement automatic scaling of static brushes to the correct size. We have scale already, but we probably need to implement a proper 2d canvas scaling algorithm. Some options to consider: linear < [bi]cubic < nohalo/lohalo
-- Note that we do NOT automatically find & register computed brushes because that's an easy way to execute arbitrary Lua code & cause a security issue unless handled very carefully

View File

@ -0,0 +1,77 @@
local we_c = worldeditadditions_commands
local wea = worldeditadditions
-- ███████ ██████ ██ ██ ██ ██████ ████████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██████ ██
-- ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██████ ███████ ██ ██
worldedit.register_command("sculpt", {
params = "[<brush_name=default> [<height=5> [<brush_size=10>]]]",
description = "Applies a sculpting brush to the terrain with a given height. See //sculptlist to list all available brushes. Note that while the brush size is configurable, the actual brush size you end up with may be slightly different to that which you request due to brush size restrictions.",
privs = { worldedit = true },
require_pos = 1,
parse = function(params_text)
if not params_text or params_text == "" then
params_text = "default"
end
local parts = wea.split_shell(params_text)
local brush_name = "default"
local height = 5
local brush_size = 10
if #parts >= 1 then
brush_name = table.remove(parts, 1)
if not wea.sculpt.brushes[brush_name] then
return false, "A brush with the name '"..brush_name.."' doesn't exist. Try using //sculptlist to list all available brushes."
end
end
if #parts >= 1 then
height = tonumber(table.remove(parts, 1))
if not height then
return false, "Invalid height value (must be an integer - negative values lower terrain instead of raising it)"
end
end
if #parts >= 1 then
brush_size = tonumber(table.remove(parts, 1))
if not brush_size or brush_size < 1 then
return false, "Invalid brush size. Brush sizes must be a positive integer."
end
end
return true, brush_name, math.floor(height), math.floor(brush_size)
end,
nodes_needed = function(name, brush_name, height, brush_size)
local success, brush, size_actual = wea.sculpt.make_brush(brush_name, brush_size)
if not success then return 0 end
-- This solution allows for brushes with negative values
-- it also allows for brushes that 'break the rules' and have values
-- that exceed the -1 to 1 range
local brush_min = wea.min(brush)
local brush_max = wea.max(brush)
local range_nodes = (brush_max * height) - (brush_min * height)
print("//sculpt range_nodes", range_nodes)
return size_actual.x * size_actual.y * range_nodes
end,
func = function(name, brush_name, height, brush_size)
local start_time = wea.get_ms_time()
local pos1, pos2 = wea.Vector3.sort(
worldedit.pos1[name],
worldedit.pos2[name]
)
local success, stats = wea.sculpt.apply(
pos1,
brush_name, height, brush_size
)
if not success then return success, stats.added end
local time_taken = wea.get_ms_time() - start_time
minetest.log("action", name .. " used //sculpt at "..pos1..", adding " .. stats.added.." nodes and removing "..stats.removed.." nodes in "..time_taken.."s")
return true, stats.added.." nodes added and "..stats.removed.." removed in "..wea.format.human_time(time_taken)
end
})