diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index 372b896..6486a56 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -44,6 +44,7 @@ dofile(worldeditadditions.modpath.."/lib/scale_down.lua") dofile(worldeditadditions.modpath.."/lib/scale.lua") dofile(worldeditadditions.modpath.."/lib/conv/conv.lua") dofile(worldeditadditions.modpath.."/lib/erode/erode.lua") +dofile(worldeditadditions.modpath.."/lib/noise/init.lua") dofile(worldeditadditions.modpath.."/lib/count.lua") diff --git a/worldeditadditions/lib/noise/apply_2d.lua b/worldeditadditions/lib/noise/apply_2d.lua index e5e9a8c..42f0fd0 100644 --- a/worldeditadditions/lib/noise/apply_2d.lua +++ b/worldeditadditions/lib/noise/apply_2d.lua @@ -29,7 +29,7 @@ function worldeditadditions.noise.apply_2d(heightmap, noise, heightmap_size, reg rescaled = rescaled * region_height * percent heightmap[i] = rescaled else - return false, "Error: Unknown apply_mode '"..apply_mode.."'" + return false, "Error: Unknown apply mode '"..apply_mode.."'" end end end diff --git a/worldeditadditions/lib/noise/engines/perlin.lua b/worldeditadditions/lib/noise/engines/perlin.lua index 387aa22..5577d9a 100644 --- a/worldeditadditions/lib/noise/engines/perlin.lua +++ b/worldeditadditions/lib/noise/engines/perlin.lua @@ -1,31 +1,5 @@ local wea = worldeditadditions - -local function BitAND(a, b) --Bitwise and - local p, c = 1, 0 - while a > 0 and b > 0 do - local ra, rb = a%2, b%2 - if ra + rb > 1 then c = c + p end - a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 - end - return c -end - -local function fade(t) - return t * t * t * (t * (t * 6 - 15) + 10) -end - -local function lerp(t, a, b) - return a + t * (b - a) -end - -local function grad(hash, x, y, z) - local h = BitAND(hash, 15) - local u = h < 8 and x or y - local v = h < 4 and y or ((h == 12 or h == 14) and x or z) - return ((h and 1) == 0 and u or - u) + ((h and 2) == 0 and v or - v) -end - --- Perlin noise generation engine. -- Original code by Ken Perlin: http://mrl.nyu.edu/~perlin/noise/ -- Port from this StackOverflow answer: https://stackoverflow.com/a/33425812/1460422 @@ -71,42 +45,107 @@ end -- yourself. function Perlin:load() for i = 1, self.size do - self.p[i] = self.permutation[i] - self.p[255 + i] = self.p[i] + self.p[i - 1] = self.permutation[i] + self.p[i + 255] = self.permutation[i] end end +--- Formats a key-value table of values as a string. +-- @param map table The table of key-value pairs to format. +-- @returns string The given table of key-value pairs formatted as a string. +local function format_map(map) + local result = {} + for key, value in pairs(map) do + table.insert(result, key.."\t"..tostring(value)) + end + return table.concat(result, "\n") +end + + --- Returns a noise value for a given point in 3D space. -- @param x number The x co-ordinate. -- @param y number The y co-ordinate. -- @param z number The z co-ordinate. -- @returns number The calculated noise value. function Perlin:noise( x, y, z ) - local X = BitAND(math.floor(x), 255) + 1 - local Y = BitAND(math.floor(y), 255) + 1 - local Z = BitAND(math.floor(z), 255) + 1 - + y = y or 0 + z = z or 0 + local xi = self:BitAND(math.floor(x), 255) + local yi = self:BitAND(math.floor(y), 255) + local zi = self:BitAND(math.floor(z), 255) + + -- print("x", x, "y", y, "z", z, "xi", xi, "yi", yi, "zi", zi) + -- print("p[xi]", self.p[xi]) x = x - math.floor(x) y = y - math.floor(y) z = z - math.floor(z) - local u = fade(x) - local v = fade(y) - local w = fade(z) - local A = self.p[X] + Y - local AA = self.p[A] + Z - local AB = self.p[A + 1] + Z - local B = self.p[X + 1] + Y - local BA = self.p[B] + Z - local BB = self.p[B + 1] + Z - - return lerp(w, lerp(v, lerp(u, grad(self.p[AA ], x, y, z ), - grad(self.p[BA ], x - 1, y, z )), - lerp(u, grad(self.p[AB ], x, y - 1, z ), - grad(self.p[BB ], x - 1, y - 1, z ))), - lerp(v, lerp(u, grad(self.p[AA + 1], x, y, z - 1), - grad(self.p[BA + 1], x - 1, y, z - 1)), - lerp(u, grad(self.p[AB + 1], x, y - 1, z - 1), - grad(self.p[BB + 1], x - 1, y - 1, z - 1)))) + local u = self:fade(x) + local v = self:fade(y) + local w = self:fade(z) + local A = self.p[xi] + yi + local AA = self.p[A] + zi + local AB = self.p[A + 1] + zi + local AAA = self.p[AA] + local ABA = self.p[AB] + local AAB = self.p[AA + 1] + local ABB = self.p[AB + 1] + + local B = self.p[xi + 1] + yi + local BA = self.p[B] + zi + local BB = self.p[B + 1] + zi + local BAA = self.p[BA] + local BBA = self.p[BB] + local BAB = self.p[BA + 1] + local BBB = self.p[BB + 1] + + -- Add 0.5 to rescale to 0 - 1 instead of -0.5 - +0.5 + return 0.5 + self:lerp(w, + self:lerp(v, + self:lerp(u, + self:grad(AAA,x,y,z), + self:grad(BAA,x-1,y,z) + ), + self:lerp(u, + self:grad(ABA,x,y-1,z), + self:grad(BBA,x-1,y-1,z) + ) + ), + self:lerp(v, + self:lerp(u, + self:grad(AAB,x,y,z-1), self:grad(BAB,x-1,y,z-1) + ), + self:lerp(u, + self:grad(ABB,x,y-1,z-1), self:grad(BBB,x-1,y-1,z-1) + ) + ) + ) end + +function Perlin:BitAND(a, b) --Bitwise and + local p, c = 1, 0 + while a > 0 and b > 0 do + local ra, rb = a%2, b%2 + if ra + rb > 1 then c = c + p end + a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 + end + return c +end + +function Perlin:fade(t) + return t * t * t * (t * (t * 6 - 15) + 10) +end + +function Perlin:lerp(t, a, b) + return a + t * (b - a) +end + +function Perlin:grad(hash, x, y, z) + local h = self:BitAND(hash, 15) + local u = h < 8 and x or y + local v = h < 4 and y or ((h == 12 or h == 14) and x or z) + return ((h and 1) == 0 and u or - u) + ((h and 2) == 0 and v or - v) +end + + return Perlin diff --git a/worldeditadditions/lib/noise/engines/perlin_test.lua b/worldeditadditions/lib/noise/engines/perlin_test.lua new file mode 100644 index 0000000..33c90e9 --- /dev/null +++ b/worldeditadditions/lib/noise/engines/perlin_test.lua @@ -0,0 +1,70 @@ +local Perlin = require("perlin") + +--- Pads str to length len with char from left +-- Adapted from the above +local function str_padstart(str, len, char) + if char == nil then char = ' ' end + return string.rep(char, len - #str) .. str +end +local function array_2d(tbl, width) + print("==== count: "..(#tbl+1)..", width:"..width.." ====") + local display_width = 1 + for _i,value in pairs(tbl) do + display_width = math.max(display_width, #tostring(value)) + end + display_width = display_width + 2 + local next = {} + for i=0, #tbl do + table.insert(next, str_padstart(tostring(tbl[i]), display_width)) + if #next == width then + print(table.concat(next, "")) + next = {} + end + end +end + + +--- Finds the minimum value in the given list. +-- @param list number[] The list (table) of numbers to find the minimum value of. +-- @returns number The minimum value in the given list. +function min(list) + if #list == 0 then return nil end + local min = nil + for i,value in ipairs(list) do + if min == nil or min > value then + min = value + end + end + return min +end + +--- Finds the maximum value in the given list. +-- @param list number[] The list (table) of numbers to find the maximum value of. +-- @returns number The maximum value in the given list. +function max(list) + if #list == 0 then return nil end + local max = nil + for i,value in ipairs(list) do + if max == nil or max < value then + max = value + end + end + return max +end + + +local p = Perlin.new() + +local result = {} +local size = { x = 10, z = 25 } + +for x = 0, size.x do + for y = 0, size.z do + result[y*size.x + x] = p:noise(x, y, 0) + end +end + +print(array_2d(result, size.x)) + +print("MAX ", max(result)) +print("MIN ", min(result)) diff --git a/worldeditadditions/lib/noise/init.lua b/worldeditadditions/lib/noise/init.lua index 588423b..5c67327 100644 --- a/worldeditadditions/lib/noise/init.lua +++ b/worldeditadditions/lib/noise/init.lua @@ -1,12 +1,14 @@ -worldeditadditions.noise = {} +local wea = worldeditadditions + +wea.noise = {} -- The command itself -dofile(worldeditadditions.modpath.."/lib/noise/noise2d.lua") +dofile(wea.modpath.."/lib/noise/run2d.lua") -- Dependencies -dofile(worldeditadditions.modpath.."/lib/noise/apply_2d.lua") -dofile(worldeditadditions.modpath.."/lib/noise/make_2d.lua") -dofile(worldeditadditions.modpath.."/lib/noise/params_apply_default.lua") +dofile(wea.modpath.."/lib/noise/apply_2d.lua") +dofile(wea.modpath.."/lib/noise/make_2d.lua") +dofile(wea.modpath.."/lib/noise/params_apply_default.lua") -- Noise generation engines -dofile(worldeditadditions.modpath.."/lib/noise/engines/perlin.lua") +wea.noise.Perlin = dofile(wea.modpath.."/lib/noise/engines/perlin.lua") diff --git a/worldeditadditions/lib/noise/make_2d.lua b/worldeditadditions/lib/noise/make_2d.lua index 8c4f52d..9fbeb91 100644 --- a/worldeditadditions/lib/noise/make_2d.lua +++ b/worldeditadditions/lib/noise/make_2d.lua @@ -10,25 +10,43 @@ -- Written with help from https://www.redblobgames.com/maps/terrain-from-noise/ -- @param size Vector An x/y vector representing the size of the noise area to generate. -- @param params table|table A table of noise params to use to generate the noise. Values that aren't specified are filled in automatically. If a table of tables is specified, it is interpreted as multiple octaves of noise to apply in sequence. -function worldeditadditions.noise.make_2d(size, params) +function worldeditadditions.noise.make_2d(size, start_pos, params) local result = {} - local generator; - if params.algorithm == "perlin" then - generator = worldeditadditions.noise.perlin.new() - else -- We don't have any other generators just yet - return false, "Error: Unknown noise algorithm '"..params.."' (available algorithms: perlin)." + print("size x", size.x, "z", size.z, "start_pos x", start_pos.x, "z", start_pos.z) + + for i, layer in ipairs(params) do + local generator + if layer.algorithm == "perlin" then + generator = worldeditadditions.noise.Perlin.new() + else -- We don't have any other generators just yet + return false, "Error: Unknown noise algorithm '"..tostring(layer.algorithm).."' in layer "..i.." of "..#params.." (available algorithms: perlin)." + end + + for x = 0, size.x do + for y = 0, size.z do + local i = y*size.x + x + + local noise_x = (x + 100000+start_pos.x+layer.offset.x) * layer.scale.x + local noise_y = (y + 100000+start_pos.z+layer.offset.z) * layer.scale.z + local noise_value = generator:noise(noise_x, noise_y, 0) + + -- print("DEBUG NOISE x", noise_x, "y", noise_y, "value", noise_value) + if type(result[i]) ~= "number" then result[i] = 0 end + result[i] = result[i] + (noise_value ^ layer.exponent) * layer.multiply + layer.add + end + end + end - for x=params.offset.x, params.offset.x+size.x do - for y=params.offset.y, params.offset.y+size.y do - local result = 0 - for i,params_octave in ipairs(params) do - result = result + (generator:noise(x * scale.x, y * scale.y, 0) ^ params.exponent) * params.multiply + params.add - end - result[y*size.x + x] = result + for x = 0, size.x do + for y = 0, size.z do + local i = y*size.x + x + result[i] = worldeditadditions.round(result[i]) end end - return result + print("NOISE\n", worldeditadditions.format.array_2d(result, size.x)) + + return true, result end diff --git a/worldeditadditions/lib/noise/params_apply_default.lua b/worldeditadditions/lib/noise/params_apply_default.lua index 65329c0..6fb8c0a 100644 --- a/worldeditadditions/lib/noise/params_apply_default.lua +++ b/worldeditadditions/lib/noise/params_apply_default.lua @@ -32,9 +32,14 @@ function worldeditadditions.noise.params_apply_default(params) if not params[1] then params = { params } end -- If params[1] is thing, this is a list of params - -- This might be a thing if we're dealingw ith multiple octaves + -- This might be a thing if we're dealing with multiple octaves for i,params_el in ipairs(params) do local default_copy = worldeditadditions.table.shallowcopy(params_default) + + -- Keyword support + if params_el.perlin then params_el.algorithm = "perlin" end + print("DEBUG params_el type", type(params_el), "RAW", params_el) + -- Apply this table to fill in the gaps worldeditadditions.table.apply( params_el, default_copy diff --git a/worldeditadditions/lib/noise/noise2d.lua b/worldeditadditions/lib/noise/run2d.lua similarity index 78% rename from worldeditadditions/lib/noise/noise2d.lua rename to worldeditadditions/lib/noise/run2d.lua index 2e82824..6e37074 100644 --- a/worldeditadditions/lib/noise/noise2d.lua +++ b/worldeditadditions/lib/noise/run2d.lua @@ -11,14 +11,15 @@ local wea = worldeditadditions --- Applies a layer of 2d noise over the terrain in the defined region. -- @param pos1 Vector pos1 of the defined region -- @param pos2 Vector pos2 of the defined region --- @param noise_params table A noise parameters table. Will be passed unmodified to PerlinNoise() from the Minetest API. -function worldeditadditions.noise.noise2d(pos1, pos2, noise_params) +-- @param noise_params table A noise parameters table. +function worldeditadditions.noise.run2d(pos1, pos2, noise_params) pos1, pos2 = worldedit.sort_pos(pos1, pos2) local region_height = pos1.y - pos2.y -- pos2 will always have the highest co-ordinates now -- Fill in the default params noise_params = worldeditadditions.noise.params_apply_default(noise_params) + print("DEBUG noise_params[1] ", wea.format.map(noise_params[1])) -- Fetch the nodes in the specified area local manip, area = worldedit.manip_helpers.init(pos1, pos2) @@ -31,15 +32,22 @@ function worldeditadditions.noise.noise2d(pos1, pos2, noise_params) ) local heightmap_new = worldeditadditions.table.shallowcopy(heightmap_old) - local noisemap = wea.noise.make_2d(noise_params, heightmap_size) + local success, noisemap = wea.noise.make_2d( + heightmap_size, + pos1, + noise_params) + if not success then return success, noisemap end - wea.noise.apply_2d( + success, message = wea.noise.apply_2d( heightmap_new, noisemap, heightmap_size, region_height, - noise_params.apply + noise_params[1].apply ) + print("RETURNED apply_2d success", success, "message", message) + if not success then return success, message end + local success, stats = wea.apply_heightmap_changes( pos1, pos2, diff --git a/worldeditadditions/utils/parse/map.lua b/worldeditadditions/utils/parse/map.lua index aa35983..33f3052 100644 --- a/worldeditadditions/utils/parse/map.lua +++ b/worldeditadditions/utils/parse/map.lua @@ -13,7 +13,7 @@ function worldeditadditions.parse.map(params_text) if i % 2 == 0 then -- Lua starts at 1 :-/ -- Try converting to a number to see if it works local part_converted = tonumber(part) - if as_number == nil then part_converted = part end + if part_converted == nil then part_converted = part end -- Look for bools if part_converted == "true" then part_converted = true end if part_converted == "false" then part_converted = false end diff --git a/worldeditadditions_commands/commands/noise2d.lua b/worldeditadditions_commands/commands/noise2d.lua new file mode 100644 index 0000000..28ca39b --- /dev/null +++ b/worldeditadditions_commands/commands/noise2d.lua @@ -0,0 +1,51 @@ + +local wea = worldeditadditions + +worldedit.register_command("noise2d", { + params = "[ []] [ []] ...]", + description = "Applies 2d random noise to the terrain as a 2d heightmap in the defined region. Optionally takes an arbitrary set of key - value pairs representing parameters that control the properties of the noise and how it's applied. See the full documentation for details of these parameters and what they do.", + privs = { worldedit = true }, + require_pos = 2, + parse = function(params_text) + if not params_text then return true, {} end + params_text = wea.trim(params_text) + if params_text == "" then return true, {} end + + + local success, map = worldeditadditions.parse.map(params_text) + if not success then return success, map end + + if map.scale then + map.scale = tonumber(map.scale) + map.scale = wea.Vector3.new(map.scale, map.scale, map.scale) + elseif map.scalex or map.scaley or map.scalez then + map.scalex = tonumber(map.scalex) or 0 + map.scaley = tonumber(map.scaley) or 0 + map.scalez = tonumber(map.scalez) or 0 + map.scale = wea.Vector3.new(map.scalex, map.scaley, map.scalez) + end + if map.offsetx or map.offsety or map.offsetz then + map.offsetx = tonumber(map.offsetx) or 0 + map.offsety = tonumber(map.offsety) or 0 + map.offsetz = tonumber(map.offsetz) or 0 + map.offset = wea.Vector3.new(map.offsetx, map.offsety, map.offsetz) + end + + return true, map + end, + nodes_needed = function(name) + return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) + end, + func = function(name, params) + local start_time = worldeditadditions.get_ms_time() + local success, stats = worldeditadditions.noise.run2d( + worldedit.pos1[name], worldedit.pos2[name], + params + ) + if not success then return success, stats end + local time_taken = worldeditadditions.get_ms_time() - start_time + + minetest.log("action", name .. " used //noise2d at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", adding " .. stats.added .. " nodes and removing " .. stats.removed .. " nodes in " .. time_taken .. "s") + return true, stats.added .. " nodes added and " .. stats.removed .. " nodes removed in " .. worldeditadditions.format.human_time(time_taken) + end +}) diff --git a/worldeditadditions_commands/init.lua b/worldeditadditions_commands/init.lua index f435b8c..3cf7e39 100644 --- a/worldeditadditions_commands/init.lua +++ b/worldeditadditions_commands/init.lua @@ -28,6 +28,7 @@ dofile(we_c.modpath.."/commands/hollow.lua") dofile(we_c.modpath.."/commands/layers.lua") dofile(we_c.modpath.."/commands/line.lua") dofile(we_c.modpath.."/commands/maze.lua") +dofile(we_c.modpath.."/commands/noise2d.lua") dofile(we_c.modpath.."/commands/overlay.lua") dofile(we_c.modpath.."/commands/replacemix.lua") dofile(we_c.modpath.."/commands/scale.lua")