mirror of
https://github.com/sbrl/Minetest-WorldEditAdditions.git
synced 2024-12-23 12:05:01 +00:00
Implement initial (untested) convolution system.
Next we need to implement a worldedit function to handle fetching the manip data, calculating the heightmap, pushing it through this convolutional system, and saving the changes back again.
This commit is contained in:
parent
35001f05f8
commit
9b9a471aa8
6 changed files with 224 additions and 2 deletions
6
worldeditadditions/lib/convolution/convolution.lua
Normal file
6
worldeditadditions/lib/convolution/convolution.lua
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
worldeditadditions.conv = {}
|
||||||
|
|
||||||
|
dofile(worldeditadditions.modpath.."/lib/conv/kernels.lua")
|
||||||
|
dofile(worldeditadditions.modpath.."/lib/conv/kernel_gaussian.lua")
|
||||||
|
|
||||||
|
dofile(worldeditadditions.modpath.."/lib/conv/convolve.lua")
|
43
worldeditadditions/lib/convolution/convolve.lua
Normal file
43
worldeditadditions/lib/convolution/convolve.lua
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Convolves over a given 2D heightmap with a given matrix.
|
||||||
|
Note that this *mutates* the given heightmap.
|
||||||
|
Note also that the dimensions of the matrix must *only* be odd.
|
||||||
|
@param {number[]} heightmap The 2D heightmap to convolve over.
|
||||||
|
@param {[number,number]} heightmap_size The size of the heightmap as [ height, width ]
|
||||||
|
@param {number[]} matrix The matrix to convolve with.
|
||||||
|
@param {[number, number]} matrix_size The size of the convolution matrix as [ height, width ]
|
||||||
|
]]--
|
||||||
|
function worldeditadditions.conv.convole(heightmap, heightmap_size, matrix, matrix_size)
|
||||||
|
if matrix_size[0] % 2 ~= 1 or matrix_size[1] % 2 ~= 1 then
|
||||||
|
return false, "Error: The matrix size must contain only odd numbers (even number detected)"
|
||||||
|
end
|
||||||
|
|
||||||
|
local border_size = {
|
||||||
|
(matrix_size[0]-1) / 2, -- height
|
||||||
|
(matrix_size[1]-1) / 2 -- width
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Convolve over only the bit that allows us to use the full convolution matrix
|
||||||
|
for y = heightmap_size[0] - border_size[0], border_size[0], -1 do
|
||||||
|
for x = heightmap_size[1] - border_size[1], border_size[1], -1 do
|
||||||
|
local total = 0
|
||||||
|
|
||||||
|
for my = matrix_size[0], 0, -1 do
|
||||||
|
for mx = matrix_size[1], 0, -1 do
|
||||||
|
local mi = (my * matrix_size[1]) + mx
|
||||||
|
local cy = y + (my - border_size[0])
|
||||||
|
local cx = x + (mx - border_size[1])
|
||||||
|
|
||||||
|
local i = (cy * heightmap_size[1]) + cx
|
||||||
|
|
||||||
|
total = total + matrix[mi] * heightmap[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
heightmap[(y * heightmap_size[1]) + x] = total
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, heightmap
|
||||||
|
end
|
51
worldeditadditions/lib/convolution/kernel_gaussian.lua
Normal file
51
worldeditadditions/lib/convolution/kernel_gaussian.lua
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
-- Ported from Javascript by Starbeamrainbowlabs
|
||||||
|
-- Original source: https://github.com/sidorares/gaussian-convolution-kernel/
|
||||||
|
-- From
|
||||||
|
-- the code is taken from https://github.com/mattlockyer/iat455/blob/6493c882f1956703133c1bffa1d7ee9a83741cbe/assignment1/assignment/effects/blur-effect-dyn.js
|
||||||
|
-- (c) Matt Lockyer, https://github.com/mattlockyer
|
||||||
|
|
||||||
|
-- hypotenuse has moved to utils/numbers.lua
|
||||||
|
|
||||||
|
--[[
|
||||||
|
* Generates a kernel used for the gaussian blur effect.
|
||||||
|
*
|
||||||
|
* @param dimension is an odd integer
|
||||||
|
* @param sigma is the standard deviation used for our gaussian function.
|
||||||
|
*
|
||||||
|
* @returns an array with dimension^2 number of numbers, all less than or equal
|
||||||
|
* to 1. Represents our gaussian blur kernel.
|
||||||
|
]]--
|
||||||
|
function worldeditadditions.conv.kernel_gaussian(dimension, sigma)
|
||||||
|
if not (dimension % 2) or math.floor(dimension) ~= dimension or dimension < 3 then
|
||||||
|
return false, "The dimension must be an odd integer greater than or equal to 3"
|
||||||
|
end
|
||||||
|
local kernel = {};
|
||||||
|
|
||||||
|
local two_sigma_square = 2 * sigma * sigma;
|
||||||
|
local centre = (dimension - 1) / 2;
|
||||||
|
|
||||||
|
local sum = 0
|
||||||
|
for i = 0, dimension-1 do
|
||||||
|
for j = 0, dimension-1 do
|
||||||
|
local distance = worldeditadditions.hypotenuse(i, j, centre, centre)
|
||||||
|
|
||||||
|
-- The following is an algorithm that came from the gaussian blur
|
||||||
|
-- wikipedia page [1].
|
||||||
|
--
|
||||||
|
-- http://en.wikipedia.org/w/index.php?title=Gaussian_blur&oldid=608793634#Mechanics
|
||||||
|
local gaussian = (1 / math.sqrt(
|
||||||
|
math.pi * two_sigma_square
|
||||||
|
)) * math.exp((-1) * (math.pow(distance, 2) / two_sigma_square));
|
||||||
|
|
||||||
|
sum = sum + gaussian
|
||||||
|
kernel[i*dimension + j] = gaussian
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns the unit vector of the kernel array.
|
||||||
|
for k,v in pairs(kernel) do
|
||||||
|
kernel[k] = kernel[k] / sum
|
||||||
|
end
|
||||||
|
|
||||||
|
return kernel
|
||||||
|
end
|
94
worldeditadditions/lib/convolution/kernels.lua
Normal file
94
worldeditadditions/lib/convolution/kernels.lua
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
--- Creates a (normalised) box convolutional kernel.
|
||||||
|
-- Larger box kernels will obviously be slower, but will produce a more blurred
|
||||||
|
-- effect (i.e. smoother terrain).
|
||||||
|
-- @param width number The width of the kernel.
|
||||||
|
-- @param height number The height of the kernel.
|
||||||
|
-- @return The resulting kernel as a ZERO-indexed list of numbers.
|
||||||
|
function worldeditadditions.conv.kernel_box(width, height)
|
||||||
|
local result = {}
|
||||||
|
local total = 0
|
||||||
|
for y = 0, height do
|
||||||
|
for x = 0, width do
|
||||||
|
result[(y*width) + x] = 1
|
||||||
|
total = total + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Ensure that everything sums up to 1
|
||||||
|
for i = 0, #result do
|
||||||
|
result[i] = result[i] / total
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Computes the Lth line of Pascal's triangle.
|
||||||
|
-- More information: https://en.wikipedia.org/wiki/Pascal%27s_triangle
|
||||||
|
-- There are probably more efficient ways to it that don't repeat themselves as
|
||||||
|
-- much, but this is my solution.
|
||||||
|
-- @param l number The 1-indexed row of Pascal's Triangle to return.
|
||||||
|
-- @return number[] A ZERO-indexed list of numbers in the specified row of Pascal's Triangle.
|
||||||
|
local function pascal(l)
|
||||||
|
local prev = {}
|
||||||
|
prev[0] = 1
|
||||||
|
if l == 1 then return prev end
|
||||||
|
prev[1] = 1
|
||||||
|
if l == 2 then return prev end
|
||||||
|
local length_last = 2
|
||||||
|
|
||||||
|
for n=3, l do
|
||||||
|
local next = {}
|
||||||
|
for i=0, length_last do
|
||||||
|
if i == 0 or i == length_last then
|
||||||
|
next[i] = 1
|
||||||
|
else
|
||||||
|
next[i] = prev[i - 1] + prev[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
prev = next
|
||||||
|
length_last = length_last + 1
|
||||||
|
end
|
||||||
|
return prev
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates a pascal convolutional kernel.
|
||||||
|
-- Larger pascal kernels will obviously be slower, but will produce a more blurred
|
||||||
|
-- effect (i.e. smoother terrain).
|
||||||
|
-- @param width number The width of the kernel.
|
||||||
|
-- @param height number The height of the kernel.
|
||||||
|
-- @param normalise=true bool Whether to normalise the resulting kernel (default: true)
|
||||||
|
-- @return The resulting kernel as a ZERO-indexed list of numbers.
|
||||||
|
function worldeditadditions.conv.kernel_pascal(width, height, normalise)
|
||||||
|
if normalise == nil then normalise = true end
|
||||||
|
|
||||||
|
local result = {}
|
||||||
|
local pascal_width = width
|
||||||
|
local height_middle = ((height - 2) / 2)
|
||||||
|
local total = 0
|
||||||
|
for y = 0, height-1 do
|
||||||
|
local pascal_numbers = pascal(pascal_width)
|
||||||
|
local pascal_start = (pascal_width - width) / 2
|
||||||
|
for x = 0, width - 1 do
|
||||||
|
result[(y*width) + x] = pascal_numbers[pascal_start + x]
|
||||||
|
total = total + pascal_numbers[pascal_start + x]
|
||||||
|
end
|
||||||
|
|
||||||
|
if y > height_middle then pascal_width = pascal_width - 2
|
||||||
|
else pascal_width = pascal_width + 2 end
|
||||||
|
end
|
||||||
|
|
||||||
|
if normalise then
|
||||||
|
for k,v in pairs(result) do
|
||||||
|
result[k] = result[k] / total
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- print("box, 5x5")
|
||||||
|
-- print_2d(kernel_box(5, 5), 5)
|
||||||
|
-- print("pascal, 5x5")
|
||||||
|
-- print_2d(kernel_pascal(5, 5), 5)
|
||||||
|
-- print("pascal, 7x7")
|
||||||
|
-- print_2d(kernel_pascal(7, 7), 7)
|
||||||
|
-- print("pascal, 9x9")
|
||||||
|
-- print_2d(kernel_pascal(9, 9), 9)
|
|
@ -3,3 +3,9 @@ function worldeditadditions.round(num, numDecimalPlaces)
|
||||||
local mult = 10^(numDecimalPlaces or 0)
|
local mult = 10^(numDecimalPlaces or 0)
|
||||||
return math.floor(num * mult + 0.5) / mult
|
return math.floor(num * mult + 0.5) / mult
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function worldeditadditions.hypotenuse(x1, y1, x2, y2)
|
||||||
|
local xSquare = math.pow(x1 - x2, 2);
|
||||||
|
local ySquare = math.pow(y1 - y2, 2);
|
||||||
|
return math.sqrt(xSquare + ySquare);
|
||||||
|
end
|
||||||
|
|
|
@ -73,10 +73,31 @@ function worldeditadditions.str_padstart(str, len, char)
|
||||||
return string.rep(char, len - #str) .. str
|
return string.rep(char, len - #str) .. str
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Equivalent to string.startsWith in JS
|
||||||
|
-- @param str string The string to operate on
|
||||||
|
-- @param start number The start string to look for
|
||||||
|
-- @returns bool Whether start is present at the beginning of str
|
||||||
function worldeditadditions.string_starts(str,start)
|
function worldeditadditions.string_starts(str,start)
|
||||||
return string.sub(str,1,string.len(start))==start
|
return string.sub(str,1,string.len(start))==start
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Prints a 2d array of numbers formatted like a JS TypedArray (e.g. like a manip node list or a convolutional kernel)
|
||||||
|
-- In other words, the numbers should be formatted as a single flat array.
|
||||||
|
-- @param tbl number[] The ZERO-indexed list of numbers
|
||||||
|
-- @param width number The width of 2D array.
|
||||||
|
function worldeditadditions.print_2d(tbl, width)
|
||||||
|
local next = {}
|
||||||
|
for i=0, #tbl do
|
||||||
|
table.insert(next, tbl[i])
|
||||||
|
if #next == width then
|
||||||
|
print(table.concat(next, "\t"))
|
||||||
|
next = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--- Turns an associative node_id → count table into a human-readable list.
|
--- Turns an associative node_id → count table into a human-readable list.
|
||||||
-- Works well with worldeditadditions.make_ascii_table().
|
-- Works well with worldeditadditions.make_ascii_table().
|
||||||
function worldeditadditions.node_distribution_to_list(distribution, nodes_total)
|
function worldeditadditions.node_distribution_to_list(distribution, nodes_total)
|
||||||
|
@ -127,8 +148,9 @@ function worldeditadditions.make_ascii_table(data, total)
|
||||||
return table.concat(result, "\n")
|
return table.concat(result, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parses a list of strings as a list of weighted nodes - e.g. like in the //mix command.
|
||||||
|
-- @param parts string[] The list of strings to parse (try worldeditadditions.split)
|
||||||
|
-- @returns table A table in the form node_name => weight.
|
||||||
function worldeditadditions.parse_weighted_nodes(parts)
|
function worldeditadditions.parse_weighted_nodes(parts)
|
||||||
local MODE_EITHER = 1
|
local MODE_EITHER = 1
|
||||||
local MODE_NODE = 2
|
local MODE_NODE = 2
|
||||||
|
|
Loading…
Reference in a new issue