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:
Starbeamrainbowlabs 2020-06-09 01:21:32 +01:00
parent 35001f05f8
commit 9b9a471aa8
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
6 changed files with 224 additions and 2 deletions

View 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")

View 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

View 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

View 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)

View file

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

View file

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