Start implementing a //sculpt command, but it's not finished or tested yet.

First up: test that our initial basic dynamic brushes work as intended 
with the //sculptlist [preview] command.

Also on the todo list: document it in the chat command reference!
This commit is contained in:
Starbeamrainbowlabs 2021-12-27 03:11:52 +00:00
parent c030acfd7e
commit 10c9d6f886
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
13 changed files with 261 additions and 1 deletions

View file

@ -57,6 +57,7 @@ dofile(wea.modpath.."/lib/spiral_circle.lua")
dofile(wea.modpath.."/lib/conv/conv.lua") dofile(wea.modpath.."/lib/conv/conv.lua")
dofile(wea.modpath.."/lib/erode/erode.lua") dofile(wea.modpath.."/lib/erode/erode.lua")
dofile(wea.modpath.."/lib/noise/init.lua") dofile(wea.modpath.."/lib/noise/init.lua")
dofile(wea.modpath.."/lib/sculpt/init.lua")
dofile(wea.modpath.."/lib/copy.lua") dofile(wea.modpath.."/lib/copy.lua")
dofile(wea.modpath.."/lib/move.lua") dofile(wea.modpath.."/lib/move.lua")

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(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

View file

@ -0,0 +1,13 @@
--- Returns a smooth gaussian brush.
-- @param size Vector3 The target size of the brush. Note that the actual size fo the brush will be different, as the gaussian function has some limitations.
-- @param sigma=2 number The 'smoothness' of the brush. Higher values are more smooth.
return function(size, sigma)
local size = math.min(size.x, size.y)
if size % 2 == 0 then size = size - 1 end
if size < 1 then
return false, "Error: Invalid brush size."
end
local success, gaussian = worldeditadditions.conv.kernel_gaussian(size, sigma)
return success, gaussian, { x = size, y = size }
end

View file

@ -0,0 +1,8 @@
local wea = worldeditadditions
local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__smooth.lua")
return function(size)
local success, brush, size_actual = __smooth(size, 2)
return success, brush, size_actual
end

View file

@ -0,0 +1,8 @@
local wea = worldeditadditions
local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__smooth.lua")
return function(size)
local success, brush, size_actual = __smooth(size, 1)
return success, brush, size_actual
end

View file

@ -0,0 +1,8 @@
local wea = worldeditadditions
local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__smooth.lua")
return function(size)
local success, brush, size_actual = __smooth(size, 5)
return success, brush, size_actual
end

View file

@ -0,0 +1,11 @@
--- Returns a simple square brush with 100% weight for every pixel.
return function(size)
local result = {}
for y=0, size.y do
for x=0, size.x do
result[y*size.x + x] = 1
end
end
return true, result, size
end

View file

@ -0,0 +1,20 @@
local wea = worldeditadditions
local sculpt = {
brushes = {
default_hard = dofile(wea.modpath.."/lib/sculpt/brushes/default_hard.lua"),
default = dofile(wea.modpath.."/lib/sculpt/brushes/default.lua"),
default_soft = dofile(wea.modpath.."/lib/sculpt/brushes/default_soft.lua"),
square = dofile(wea.modpath.."/lib/sculpt/brushes/square.lua")
},
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 = dofile(wea.modpath.."/lib/sculpt/apply.lua")
}
-- 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
-- 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,24 @@
local wea = worldeditadditions
--- Makes a sculpting brush that is as close to a target size as possible.
-- @param brush_name string The name of the brush to create.
-- @param target_size Vector3 The target size of the brush to create.
-- @returns true,table,Vector3|false,string If the operation was successful, true followed by the brush in a 1D ZERO-indexed table followed by the actual size of the brush as a Vector3 (x & y components used only). If the operation was not successful, false and an error message string is returned instead.
local function make_brush(brush_name, target_size)
if not wea.sculpt.brushes[brush_name] then return false, "Error: That brush does not exist. Try using //sculptbrushes to list all available sculpting brushes." end
local brush_def = wea.sculpt.brushes[brush_name]
local success, brush, size_actual
if type(brush_def) == "function" then
success, brush, size_actual = brush_def(target_size)
if not success then return success, brush end
else
brush = brush_def.brush
size_actual = brush_def.size
end
return true, brush, size_actual
end
return make_brush

View file

@ -0,0 +1,46 @@
local wea = worldeditadditions
local Vector3 = wea.Vector3
local make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua")
--- Generates a textual preview of a given brush.
-- @param brush_name string The name of the brush to create a preview for.
-- @param target_size Vector3 The target size of the brush to create. Default: (10, 10, 0).
-- @returns bool,string If the operation was successful, true followed by a preview of the brush as a string. If the operation was not successful, false and an error message string is returned instead.
local function preview_brush(brush_name, target_size)
if not target_size then target_size = Vector3.new(10, 10, 0) end
local success, brush, brush_size = make_brush(brush_name, target_size)
-- Values to map brush pixel values to.
-- Brush pixel values are first multiplied by 10 before comparing to these numbers
local values = {}
values["@"] = 9.5
values["#"] = 8
values["="] = 6
values[":"] = 5
values["-"] = 4
values["."] = 1
values[" "] = 0
local result = {}
for z = target_size.z, 0, -1 do
local row = {}
for x = target_size.x, 0, -1 do
local i = z*brush_size.x + x
local pixel = " "
local threshold_cur = -1
for value,threshold in pairs(values) do
if brush[i] > threshold and threshold_cur < threshold then
pixel = value
threshold_cur = threshold
end
end
table.insert(row, pixel)
end
table.insert(result, table.concat(row))
end
return true, table.concat(result)
end
return preview_brush

View file

@ -0,0 +1,11 @@
return function(filepath)
local brush_size = { x = 0, y = 0 }
local brush = { }
-- TODO: Import brush here
return false, "Error: Not implemented yet"
-- return true, brush, brush_size
end

View file

@ -0,0 +1,54 @@
local wea = worldeditadditions
-- ███████ ██████ ██ ██ ██ ██████ ████████ ██ ██ ███████ ████████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██████ ██ ██ ██ ███████ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██████ ██████ ███████ ██ ██ ███████ ██ ███████ ██
minetest.register_chatcommand("/sculptlist", {
params = "[preview]",
description = "Lists all the currently registered sculpting brushes and their associated metadata. If the keyword preview is specified as an argument, a preview of each brush is also printed.",
privs = { worldedit = true },
func = function(name, params_text)
if name == nil then return end
if not params_text then params_text = "" end
params_text = wea.trim(params_text)
local msg = {}
table.insert(msg, "Currently registered sculpting brushes:\n")
if params_text == "preview" then
for brush_name, brush_def in pairs(wea.sculpt.brushes) do
local preview = wea.sculpt.preview_brush(brush_name)
local brush_size = "dynamic"
if type(brush_size) ~= "function" then
brush_size = brush_def.size
end
table.insert(msg, brush_name.." ["..brush_size.."]:\n")
table.insert(msg, preview.."\n\n")
end
else
local display = { { "Name", "Native Size" } }
for brush_name, brush_def in pairs(wea.sculpt.brushes) do
local brush_size = "dynamic"
if type(brush_size) ~= "function" then
brush_size = brush_def.size
end
table.insert(display, {
brush_name,
brush_size
})
end
-- Sort by brush name
table.sort(display, function(a, b) return a[1] < b[1] end)
table.insert(msg, worldeditadditions.format.make_ascii_table(display))
end
worldedit.player_notify(name, table.concat(msg))
end
})

View file

@ -55,8 +55,9 @@ dofile(we_c.modpath.."/commands/wireframe/init.lua")
dofile(we_c.modpath.."/commands/extra/saplingaliases.lua") dofile(we_c.modpath.."/commands/extra/saplingaliases.lua")
dofile(we_c.modpath.."/commands/extra/basename.lua") dofile(we_c.modpath.."/commands/extra/basename.lua")
dofile(we_c.modpath.."/commands/extra/sculptlist.lua")
-- Don't registry the //bonemeal command if the bonemeal mod isn't present -- Don't register the //bonemeal command if the bonemeal mod isn't present
if minetest.global_exists("bonemeal") then if minetest.global_exists("bonemeal") then
dofile(we_c.modpath.."/commands/bonemeal.lua") dofile(we_c.modpath.."/commands/bonemeal.lua")
dofile(we_c.modpath.."/commands/forest.lua") dofile(we_c.modpath.."/commands/forest.lua")
@ -94,6 +95,7 @@ worldedit.alias_command("mfacing", "mface")
-- These are commented out for now, as they could be potentially dangerous to stability -- These are commented out for now, as they could be potentially dangerous to stability
-- Thorough testing is required of our replacement commands before these are uncommented -- Thorough testing is required of our replacement commands before these are uncommented
-- TODO: Depend on worldeditadditions_core before uncommenting this -- TODO: Depend on worldeditadditions_core before uncommenting this
-- BUG: //move+ seems to be leaving stuff behind for some strange reason --@sbrl 2021-12-26
-- worldeditadditions_core.alias_override("copy", "copy+") -- worldeditadditions_core.alias_override("copy", "copy+")
-- worldeditadditions_core.alias_override("move", "move+") -- MAY have issues where it doesn't overwrite the old region properly, but haven't been able to reliably reproduce this -- worldeditadditions_core.alias_override("move", "move+") -- MAY have issues where it doesn't overwrite the old region properly, but haven't been able to reliably reproduce this
-- worldeditadditions_core.alias_override("replace", "replacemix") -- worldeditadditions_core.alias_override("replace", "replacemix")