465 lines
15 KiB
Lua
465 lines
15 KiB
Lua
-- If true, the Perlin test nodes will support color
|
|
-- (set to false in case of performance problems)
|
|
local COLORIZE_NODES = true
|
|
|
|
local S = minetest.get_translator("perlin_explorer")
|
|
local F = minetest.formspec_escape
|
|
|
|
-- Holds the currently used Perlin noise
|
|
local current_perlin = {}
|
|
-- holds the current PerlinNoise object
|
|
current_perlin.noise = nil
|
|
current_perlin.noiseparams = {
|
|
offset = 0.0,
|
|
scale = 1.0,
|
|
spread = vector.new(10, 10, 10),
|
|
seed = 0,
|
|
octaves = 2,
|
|
persistence = 0.7,
|
|
lacunarity = 2.0,
|
|
}
|
|
-- Side length of calculated perlin area
|
|
current_perlin.size = nil
|
|
-- Theoretical min and max values for Perlin noise (for colorization)
|
|
current_perlin.max = 1
|
|
current_perlin.min = -1
|
|
|
|
-- dimensions of current Perlin noise (2 or 3)
|
|
current_perlin.dimensions = 2
|
|
-- If greater than 1, the Perlin noise values are "pixelized". Noise values at
|
|
-- coordinates not divisible by sidelen will be set equal to the noise value
|
|
-- of the nearest number (counting downwards) that is divisible by sidelen.
|
|
-- This is (kind of) analogous to the "sidelen" parameter of mapgen decorations.
|
|
current_perlin.sidelen = 1
|
|
|
|
------------
|
|
|
|
-- Helper functions
|
|
-- Reduce the pos coordinates down to the closest numbers divisible by sidelen
|
|
local sidelen_pos = function(pos, sidelen)
|
|
local newpos = table.copy(pos)
|
|
if sidelen <= 1 then
|
|
return newpos
|
|
end
|
|
newpos.x = newpos.x - newpos.x % sidelen
|
|
newpos.y = newpos.y - newpos.y % sidelen
|
|
newpos.z = newpos.z - newpos.z % sidelen
|
|
return newpos
|
|
end
|
|
|
|
-- Test nodes to generate a map based on a perlin noise
|
|
local paramtype2
|
|
if COLORIZE_NODES then
|
|
paramtype2 = "color"
|
|
end
|
|
minetest.register_node("perlin_explorer:node", {
|
|
description = S("Perlin Test Node"),
|
|
paramtype = "light",
|
|
sunlight_propagates = true,
|
|
paramtype2 = paramtype2,
|
|
tiles = {"perlin_explorer_node.png"},
|
|
palette = "perlin_explorer_node_palette.png",
|
|
groups = { dig_immediate = 3 },
|
|
-- Force-drop without metadata to avoid spamming the inventory
|
|
drop = "perlin_explorer:node",
|
|
})
|
|
minetest.register_node("perlin_explorer:node_negative", {
|
|
description = S("Negative Perlin Test Node"),
|
|
paramtype = "light",
|
|
sunlight_propagates = true,
|
|
paramtype2 = paramtype2,
|
|
tiles = {"perlin_explorer_node_neg.png"},
|
|
palette = "perlin_explorer_node_palette_neg.png",
|
|
groups = { dig_immediate = 3 },
|
|
-- Force-drop without metadata to avoid spamming the inventory
|
|
drop = "perlin_explorer:node_negative",
|
|
})
|
|
minetest.register_tool("perlin_explorer:getter", {
|
|
description = S("Perlin Value Getter"),
|
|
_tt_help = S("Punch a node to display the Perlin noise value at this position"),
|
|
inventory_image = "perlin_explorer_getter.png",
|
|
wield_image = "perlin_explorer_getter.png",
|
|
groups = { disable_repair = 1 },
|
|
on_use = function(itemstack, user, pointed_thing)
|
|
if not user then
|
|
return
|
|
end
|
|
local privs = minetest.get_player_privs(user:get_player_name())
|
|
if not privs.server then
|
|
minetest.chat_send_player(user:get_player_name(), S("Insufficient privileges! You need the @1 privilege to use this tool.", "server"))
|
|
return
|
|
end
|
|
if current_perlin.noise then
|
|
if pointed_thing.type ~= "node" then
|
|
-- No-op for non-nodes
|
|
return
|
|
end
|
|
local pos = pointed_thing.under
|
|
local getpos = sidelen_pos(pos, current_perlin.sidelen)
|
|
local val
|
|
if current_perlin.dimensions == 2 then
|
|
val = current_perlin.noise:get_2d({x=getpos.x, y=getpos.z})
|
|
elseif current_perlin.dimensions == 3 then
|
|
val = current_perlin.noise:get_3d(getpos)
|
|
else
|
|
minetest.chat_send_player(user:get_player_name(), S("Unknown/invalid number of Perlin noise dimensions. Use /generate_perlin first!"))
|
|
end
|
|
minetest.chat_send_player(user:get_player_name(), S("pos=@1, value=@2", minetest.pos_to_string(pos), val))
|
|
else
|
|
local msg = S("No Perlin noise set. Set one with /set_perlin_noise!")
|
|
minetest.chat_send_player(user:get_player_name(), msg)
|
|
end
|
|
end,
|
|
})
|
|
|
|
local CONTENT_TEST_NODE = minetest.get_content_id("perlin_explorer:node")
|
|
local CONTENT_TEST_NODE_NEGATIVE = minetest.get_content_id("perlin_explorer:node_negative")
|
|
|
|
local update_map = function(pos, set_nodes)
|
|
local stats
|
|
if not current_perlin.noise then
|
|
return
|
|
end
|
|
local size_v = vector.new(current_perlin.size, current_perlin.size, current_perlin.size)
|
|
|
|
local startpos = pos
|
|
local endpos = vector.add(startpos, current_perlin.size-1)
|
|
|
|
local y_max = endpos.y - startpos.y
|
|
if current_perlin.dimensions == 2 then
|
|
y_max = 0
|
|
startpos.y = pos.y
|
|
endpos.y = pos.y
|
|
end
|
|
|
|
local vmanip, emin, emax, vdata, vdata2, varea
|
|
if set_nodes then
|
|
vmanip = VoxelManip(startpos, endpos)
|
|
emin, emax = vmanip:get_emerged_area()
|
|
vdata = vmanip:get_data()
|
|
vdata2 = vmanip:get_param2_data()
|
|
varea = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
|
|
end
|
|
|
|
stats = {}
|
|
stats.avg = 0
|
|
local sum_of_values = 0
|
|
local value_count = 0
|
|
for x=0, endpos.x - startpos.x do
|
|
for y=0, y_max do
|
|
for z=0, endpos.z - startpos.z do
|
|
-- Get Perlin value at current pos
|
|
local relpos = vector.new(x,y,z)
|
|
local abspos = vector.add(startpos, relpos)
|
|
local abspos_get = sidelen_pos(abspos, current_perlin.sidelen)
|
|
local perlin_value
|
|
if current_perlin.dimensions == 2 then
|
|
perlin_value = current_perlin.noise:get_2d({x=abspos_get.x, y=abspos_get.z})
|
|
elseif current_perlin.dimensions == 3 then
|
|
perlin_value = current_perlin.noise:get_3d(abspos_get)
|
|
else
|
|
error("[perlin_explorer] Unknown/invalid number of Perlin noise dimensions!")
|
|
return
|
|
end
|
|
|
|
-- Statistics
|
|
if not stats.min then
|
|
stats.min = perlin_value
|
|
elseif perlin_value < stats.min then
|
|
stats.min = perlin_value
|
|
end
|
|
if not stats.max then
|
|
stats.max = perlin_value
|
|
elseif perlin_value > stats.max then
|
|
stats.max = perlin_value
|
|
end
|
|
sum_of_values = sum_of_values + perlin_value
|
|
value_count = value_count + 1
|
|
|
|
-- Calculate color (param2) for node
|
|
local zeropoint = 0
|
|
local min_size = zeropoint - current_perlin.min
|
|
local max_size = current_perlin.max - zeropoint
|
|
local node_param2
|
|
if perlin_value >= zeropoint then
|
|
node_param2 = (math.abs(perlin_value) / max_size) * 255
|
|
else
|
|
node_param2 = (math.abs(perlin_value) / min_size) * 255
|
|
end
|
|
node_param2 = math.floor(math.abs(node_param2))
|
|
node_param2 = math.max(0, math.min(255, node_param2))
|
|
|
|
if set_nodes then
|
|
-- Get vmanip index
|
|
local index = varea:indexp(abspos)
|
|
if not index then
|
|
return
|
|
end
|
|
|
|
-- Set node and param2
|
|
if perlin_value >= zeropoint then
|
|
vdata[index] = CONTENT_TEST_NODE
|
|
vdata2[index] = node_param2
|
|
else
|
|
if current_perlin.show_negative == true then
|
|
vdata[index] = CONTENT_TEST_NODE_NEGATIVE
|
|
vdata2[index] = node_param2
|
|
else
|
|
vdata[index] = minetest.CONTENT_AIR
|
|
vdata2[index] = 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
stats.avg = sum_of_values / value_count
|
|
|
|
if set_nodes then
|
|
-- Set vmanip, return stats
|
|
vmanip:set_data(vdata)
|
|
vmanip:set_param2_data(vdata2)
|
|
vmanip:write_to_map()
|
|
end
|
|
return stats
|
|
end
|
|
|
|
-- Creates and demonstrates a Perlin noise.
|
|
-- * pos: Where the Perlin noise starts
|
|
-- * options: table with:
|
|
-- * dimensions: number of Perlin noise dimensions (2 or 3)
|
|
-- * size: side length of area/volume to calculate)
|
|
-- * show_negative: if true, places nodes for negative Perlin values (default: true for 2 dimensions and false for 3 dimensions)
|
|
-- * set_nodes: if true, will set nodes, otherwise it's a "dry run"
|
|
local create_perlin = function(pos, options)
|
|
if not current_perlin.noise then
|
|
return false
|
|
end
|
|
current_perlin.dimensions = options.dimensions
|
|
current_perlin.size = options.size
|
|
current_perlin.show_negative = options.show_negative
|
|
if current_perlin.show_negative == nil then
|
|
if current_perlin.dimensions == 2 then
|
|
current_perlin.show_negative = true
|
|
elseif current_perlin.dimensions == 3 then
|
|
current_perlin.show_negative = false
|
|
end
|
|
end
|
|
local mpos = vector.round(pos)
|
|
local set_nodes = options.set_nodes ~= false
|
|
local stats = update_map(mpos, set_nodes)
|
|
if stats then
|
|
return string.format("Perlin noise created! Stats: min. value=%.3f, max. value=%.3f, avg. value=%.3f", stats.min, stats.max, stats.avg)
|
|
end
|
|
end
|
|
|
|
-- Sets the currently active Perlin noise.
|
|
-- * noiseparams: NoiseParams table (see Minetest's Lua API documentation)
|
|
local set_perlin_noise = function(noiseparams)
|
|
current_perlin.noise = PerlinNoise(noiseparams)
|
|
current_perlin.noiseparams = noiseparams
|
|
end
|
|
|
|
minetest.register_chatcommand("perlin_set_options", {
|
|
privs = { server = true },
|
|
description = S("Set Perlin options"),
|
|
params = S("<dimensions> <sidelen> <minval> <maxval>"),
|
|
func = function(name, param)
|
|
local dimensions, sidelen, min, max = string.match(param, "([23]) ([0-9]+) ([0-9.-]+) ([0-9.-]+)")
|
|
if not dimensions then
|
|
return false
|
|
end
|
|
dimensions = tonumber(dimensions)
|
|
sidelen = tonumber(sidelen)
|
|
min = tonumber(min)
|
|
max = tonumber(max)
|
|
if not dimensions or not sidelen or not min or not max then
|
|
return false, S("Invalid parameter type.")
|
|
end
|
|
current_perlin.dimensions = dimensions
|
|
current_perlin.sidelen = sidelen
|
|
current_perlin.min = min
|
|
current_perlin.max = max
|
|
return true, S("Perlin options set!")
|
|
end,
|
|
})
|
|
|
|
minetest.register_chatcommand("perlin_set_noise", {
|
|
privs = { server = true },
|
|
description = S("Set Perlin noise parameters"),
|
|
params = S("<octaves> <offset> <scale> <spread_x> <spread_y> <spread_z> <persistence> <lacunarity> <seed>"),
|
|
func = function(name, param)
|
|
local octaves, offset, scale, sx, sy, sz, persistence, lacunarity, seed = string.match(param, string.rep("([0-9.-]+) ", 8) .. "([0-9.-]+)")
|
|
if not octaves then
|
|
return false
|
|
end
|
|
octaves = tonumber(octaves)
|
|
offset = tonumber(offset)
|
|
sx = tonumber(sx)
|
|
sy = tonumber(sy)
|
|
sz = tonumber(sz)
|
|
persistence = tonumber(persistence)
|
|
lacunarity = tonumber(lacunarity)
|
|
seed = tonumber(seed)
|
|
if not octaves or not offset or not sx or not sy or not sz or not persistence or not lacunarity or not seed then
|
|
return false, S("Invalid parameter type.")
|
|
end
|
|
set_perlin_noise({
|
|
octaves = octaves,
|
|
offset = offset,
|
|
scale = scale,
|
|
spread = { x = sx, y = sy, z = sz },
|
|
persistence = persistence,
|
|
lacunarity = lacunarity,
|
|
seed = seed,
|
|
})
|
|
return true, S("Perlin noise set!")
|
|
end,
|
|
})
|
|
|
|
minetest.register_chatcommand("perlin_generate", {
|
|
privs = { server = true },
|
|
description = S("Generate Perlin noise"),
|
|
params = S("<pos> <size>"),
|
|
func = function(name, param)
|
|
local x, y, z, size = string.match(param, "([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9]+)")
|
|
if not x then
|
|
return false
|
|
end
|
|
x = tonumber(x)
|
|
y = tonumber(y)
|
|
z = tonumber(z)
|
|
size = tonumber(size)
|
|
if not x or not y or not z or not size then
|
|
return false
|
|
end
|
|
if not x or not y or not z or not size then
|
|
return false, S("Invalid parameter type.")
|
|
end
|
|
local pos = vector.new(x, y, z)
|
|
|
|
minetest.chat_send_player(name, S("Creating Perlin noise, please wait …"))
|
|
local msg = create_perlin(pos, {dimensions=current_perlin.dimensions, size=size})
|
|
if msg == false then
|
|
return false, S("No Perlin noise set. Set one with '/perlin_set_noise' first!")
|
|
end
|
|
return true, msg
|
|
end,
|
|
})
|
|
|
|
local show_formspec = function(player)
|
|
local offset = tostring(current_perlin.noiseparams.offset or "")
|
|
local scale = tostring(current_perlin.noiseparams.scale or "")
|
|
local seed = tostring(current_perlin.noiseparams.seed or "")
|
|
local sx, sy, sz = "", "", ""
|
|
if current_perlin.noiseparams.spread then
|
|
sx = tostring(current_perlin.noiseparams.spread.x or "")
|
|
sy = tostring(current_perlin.noiseparams.spread.y or "")
|
|
sz = tostring(current_perlin.noiseparams.spread.z or "")
|
|
end
|
|
local octaves = tostring(current_perlin.noiseparams.octaves or "")
|
|
local persistence = tostring(current_perlin.noiseparams.persistence or "")
|
|
local lacunarity = tostring(current_perlin.noiseparams.lacunarity or "")
|
|
-- TODO: Add 3D
|
|
-- TODO: Add pos
|
|
-- TODO: Add noise options
|
|
local form = [[
|
|
formspec_version[4]size[10,10]
|
|
field[0.5,0.5;3,0.75;offset;]]..F(S("Offset"))..[[;]]..offset..[[]
|
|
field[3.5,0.5;3,0.75;scale;]]..F(S("Scale"))..[[;]]..scale..[[]
|
|
field[6.5,0.5;3,0.75;seed;]]..F(S("Seed"))..[[;]]..seed..[[]
|
|
field[0.5,1.75;3,0.75;spread_x;]]..F(S("X Spread"))..[[;]]..sx..[[]
|
|
field[3.5,1.75;3,0.75;spread_y;]]..F(S("Y Spread"))..[[;]]..sy..[[]
|
|
field[6.5,1.75;3,0.75;spread_z;]]..F(S("Z Spread"))..[[;]]..sz..[[]
|
|
field[0.5,3;3,0.75;octaves;]]..F(S("Octaves"))..[[;]]..octaves..[[]
|
|
field[3.5,3;3,0.75;persistence;]]..F(S("Persistence"))..[[;]]..persistence..[[]
|
|
field[6.5,3;3,0.75;lacunarity;]]..F(S("Lacunarity"))..[[;]]..lacunarity..[[]
|
|
|
|
field_close_on_enter[offset;false]
|
|
field_close_on_enter[scale;false]
|
|
field_close_on_enter[seed;false]
|
|
field_close_on_enter[spread_x;false]
|
|
field_close_on_enter[spread_y;false]
|
|
field_close_on_enter[spread_z;false]
|
|
field_close_on_enter[octaves;false]
|
|
field_close_on_enter[persistence;false]
|
|
field_close_on_enter[lacunarity;false]
|
|
|
|
label[0.5,4.5;]]..F(S("Dimensions"))..[[]
|
|
dropdown[3.5,4.5;3;dimensions;]]..F(S("2D"))..[[;1;true]
|
|
|
|
button[0.5,8.5;3,1;apply;]]..F(S("Apply"))..[[]
|
|
button[3.5,8.5;3,1;create;]]..F(S("Apply and create"))..[[]
|
|
button_exit[6.5,8.5;3,1;close;]]..F(S("Close"))..[[]
|
|
]]
|
|
minetest.show_formspec(player:get_player_name(), "perlin_explorer:creator", form)
|
|
end
|
|
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
if formname ~= "perlin_explorer:creator" then
|
|
return
|
|
end
|
|
-- Require 'server' priv
|
|
local privs = minetest.get_player_privs(player:get_player_name())
|
|
if not privs.server then
|
|
return
|
|
end
|
|
local do_apply = fields.apply ~= nil
|
|
local do_create = fields.create ~= nil or fields.key_enter_field ~= nil
|
|
if (do_create or do_apply) then
|
|
if fields.offset and fields.scale and fields.seed and fields.spread_x and fields.spread_y and fields.spread_z and fields.octaves and fields.persistence and fields.lacunarity then
|
|
local offset = tonumber(fields.offset)
|
|
local scale = tonumber(fields.scale)
|
|
local seed = tonumber(fields.seed)
|
|
local sx = tonumber(fields.spread_x)
|
|
local sy = tonumber(fields.spread_y)
|
|
local sz = tonumber(fields.spread_z)
|
|
local spread = vector.new(sx, sy, sz)
|
|
local octaves = tonumber(fields.octaves)
|
|
local persistence = tonumber(fields.persistence)
|
|
local lacunarity = tonumber(fields.lacunarity)
|
|
local dimensions = 2 -- TODO
|
|
if not (offset and scale and spread_x and spread_y and spread_z and octaves and persistence and dimensions) then
|
|
set_perlin_noise({
|
|
offset = offset,
|
|
scale = scale,
|
|
seed = seed,
|
|
spread = spread,
|
|
octaves = octaves,
|
|
persistence = persistence,
|
|
lacunarity = lacunarity,
|
|
})
|
|
if do_create then
|
|
local pos = player:get_pos()
|
|
pos.y = pos.y - 10
|
|
local msg = create_perlin(pos, {dimensions=dimensions, size=64})
|
|
if msg == false then
|
|
minetest.log("error", "[perlin_explorer] Error generating Perlin noise nodes!")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
minetest.register_tool("perlin_explorer:creator", {
|
|
description = S("Perlin Noise Creator"),
|
|
_tt_help = S("Punch to open the Perlin noise creation menu"),
|
|
inventory_image = "perlin_explorer_creator.png",
|
|
wield_image = "perlin_explorer_creator.png",
|
|
groups = { disable_repair = 1 },
|
|
on_use = function(itemstack, user, pointed_thing)
|
|
if not user then
|
|
return
|
|
end
|
|
local privs = minetest.get_player_privs(user:get_player_name())
|
|
if not privs.server then
|
|
minetest.chat_send_player(user:get_player_name(), S("Insufficient privileges! You need the @1 privilege to use this tool.", "server"))
|
|
return
|
|
end
|
|
show_formspec(user)
|
|
end,
|
|
})
|
|
|
|
|