399 lines
12 KiB
Lua
399 lines
12 KiB
Lua
-- The main mapgen code.
|
|
|
|
--[[ THIS FILE WILL BE RUN TWICE!
|
|
1) In the global Luanti environment,
|
|
2) In the heavily-restricted threaded mapgen environment
|
|
|
|
This means this file is very restricted and only has
|
|
access to functions that are available in both the
|
|
global and the mapgen environment. Refer to Luanti's
|
|
Lua API documentation for details.
|
|
|
|
In the global environment, this file runs to expose the
|
|
function lzr_mapgen.generate_piece, but it does not call
|
|
minetest.register_on_generated.
|
|
|
|
In the mapgen environment, it uses
|
|
minetest.register_on_generated, but it does not modify
|
|
the global environment.
|
|
|
|
The reason this file is called twice is to avoid code
|
|
duplication.
|
|
]]
|
|
|
|
-- This variable will be true when we're in the mapgen
|
|
-- environment. The mapgen environment does not have
|
|
-- access to global variables, so if lzr_mapgen does
|
|
-- not exist, we can decude we must be the mapgen
|
|
-- environment.
|
|
local IS_IN_MAPGEN_ENVIRONMENT = not minetest.global_exists("lzr_mapgen")
|
|
|
|
|
|
--[[ MAPGEN OVERVIEW:
|
|
The mapgen has two main zones:
|
|
|
|
Deep Ocean: The Deep Ocean is a very simple zone and very fast
|
|
to generate, it's just a flat layer of water with a flat seabed
|
|
way below the surface. Perfect to spawn the ship in.
|
|
|
|
Islands: Islands is a beautiful zone using 2D Perlin
|
|
noise, featuring tropical islands, palms, plants, hills, and
|
|
a “hilly” ocean floor. It is derived from the [islands] mod
|
|
by TheTermos, released under the MIT License in 2019.
|
|
|
|
Both zones are separated on the Z axis by DEEP_OCEAN_Z.
|
|
|
|
There is a hard ugly seam between both zones, so it is
|
|
important the game action happens far away from this
|
|
seam.
|
|
]]
|
|
|
|
|
|
-- Some basic values for mapgen coordinates
|
|
|
|
-- Water will be at this Y level, all the way down to SEABED_LEVEL+1
|
|
local WATER_LEVEL = 1
|
|
-- The seabed will be at this Y level, all the way down to SEASTONE_LEVEL+1 (deep ocean only)
|
|
local SEABED_LEVEL = -1000
|
|
-- The seastone will be at and below this Y level, all the way down to the bottom of the world (deep ocean only)
|
|
local SEASTONE_LEVEL = -1002
|
|
|
|
-- The deep ocean will begin when the Z coordinate is this value or lower.
|
|
-- WARNING: This value is duplicated in lzr_globals.
|
|
-- When you change it, you MUST also change it in lzr_globals!
|
|
local DEEP_OCEAN_Z = -20000
|
|
|
|
local floor = math.floor
|
|
local ceil = math.ceil
|
|
local min = math.min
|
|
local max = math.max
|
|
local random = math.random
|
|
|
|
local convex = false
|
|
|
|
local mult = 1.0
|
|
-- Set the 3D noise parameters for the terrain.
|
|
|
|
|
|
local np_terrain = {
|
|
offset = -11*mult, -- ratio 2:7 or 1:4 ?
|
|
scale = 40*mult,
|
|
spread = {x = 256*mult, y =256*mult, z = 256*mult},
|
|
seed = 1234,
|
|
octaves = convex and 1 or 5,
|
|
persist = 0.38,
|
|
lacunarity = 2.33,
|
|
}
|
|
|
|
local np_var = {
|
|
offset = 0,
|
|
scale = 6*mult,
|
|
spread = {x = 64*mult, y =64*mult, z = 64*mult},
|
|
seed = 567891,
|
|
octaves = 4,
|
|
persist = 0.4,
|
|
lacunarity = 1.89,
|
|
}
|
|
|
|
local np_hills = {
|
|
offset = 2.5, -- off/scale ~ 2:3
|
|
scale = -3.5,
|
|
spread = {x = 64*mult, y =64*mult, z = 64*mult},
|
|
seed = 2345,
|
|
octaves = 3,
|
|
persist = 0.40,
|
|
lacunarity = 2.0,
|
|
flags = "absvalue"
|
|
}
|
|
|
|
local np_cliffs = {
|
|
offset = 0,
|
|
scale = 0.72,
|
|
spread = {x = 180*mult, y =180*mult, z = 180*mult},
|
|
seed = 78901,
|
|
octaves = 2,
|
|
persist = 0.4,
|
|
lacunarity = 2.11,
|
|
}
|
|
|
|
local np_trees = {
|
|
offset = - 0.003,
|
|
scale = 0.008,
|
|
spread = {x = 64, y = 64, z = 64},
|
|
seed = 2,
|
|
octaves = 5,
|
|
persist = 1,
|
|
lacunarity = 1.91,
|
|
}
|
|
|
|
local hills_offset = np_hills.spread.x*0.5
|
|
local hills_thresh = floor((np_terrain.scale)*0.5)
|
|
local shelf_thresh = floor((np_terrain.scale)*0.5)
|
|
local cliffs_thresh = 10
|
|
|
|
local function max_height(noiseprm)
|
|
local height = 0
|
|
local scale = noiseprm.scale
|
|
for i=1,noiseprm.octaves do
|
|
height=height + scale
|
|
scale = scale * noiseprm.persist
|
|
end
|
|
return height+noiseprm.offset
|
|
end
|
|
|
|
local function min_height(noiseprm)
|
|
local height = 0
|
|
local scale = noiseprm.scale
|
|
for i=1,noiseprm.octaves do
|
|
height=height - scale
|
|
scale = scale * noiseprm.persist
|
|
end
|
|
return height+noiseprm.offset
|
|
end
|
|
|
|
local base_min = min_height(np_terrain)
|
|
local base_max = max_height(np_terrain)
|
|
local base_rng = base_max-base_min
|
|
local easing_factor = 1/(base_max*base_max*4)
|
|
local base_heightmap = {}
|
|
local result_heightmap = {}
|
|
|
|
|
|
-- Get the content IDs for the nodes used.
|
|
|
|
local c_stone = minetest.get_content_id("lzr_core:stone")
|
|
local c_sand = minetest.get_content_id("lzr_core:sand")
|
|
local c_sand_dark = minetest.get_content_id("lzr_core:seabed")
|
|
local c_dirt = minetest.get_content_id("lzr_core:dirt")
|
|
local c_dirt_g = minetest.get_content_id("lzr_core:dirt_with_grass")
|
|
local c_dirt_l = minetest.get_content_id("lzr_core:dirt_with_jungle_litter")
|
|
local c_snow = minetest.get_content_id("lzr_core:dirt") -- no snow
|
|
local c_water = minetest.get_content_id("lzr_core:water_source")
|
|
|
|
|
|
|
|
local function get_terrain_height(theight,hheight,cheight)
|
|
-- parabolic gradient
|
|
if theight > 0 and theight < shelf_thresh then
|
|
theight = theight * (theight*theight/(shelf_thresh*shelf_thresh)*0.5 + 0.5)
|
|
end
|
|
-- hills
|
|
if theight > hills_thresh then
|
|
theight = theight + max((theight-hills_thresh) * hheight,0)
|
|
-- cliffs
|
|
elseif theight > 1 and theight < hills_thresh then
|
|
local clifh = max(min(cheight,1),0)
|
|
if clifh > 0 then
|
|
clifh = -1*(clifh-1)*(clifh-1) + 1
|
|
theight = theight + (hills_thresh-theight) * clifh * ((theight<2) and theight-1 or 1)
|
|
end
|
|
end
|
|
return theight
|
|
end
|
|
|
|
-- Given an Y coordinate, returns the expected day light level
|
|
-- for water at this coordinate, assuming there are no other
|
|
-- obstacles
|
|
local get_ocean_light_level = function(y)
|
|
local ll = 15 - (WATER_LEVEL - (y-1))
|
|
return max(0, min(15, ll))
|
|
end
|
|
|
|
-- Change the day component of the given original param1
|
|
-- light value (0-255) to new_light_day (0-15) and
|
|
-- returns the result.
|
|
local change_day_light = function(original_light, new_light_day)
|
|
local light = original_light
|
|
local light_night = bit.band(light, 0xF0)
|
|
light = bit.bor(new_light_day, light_night)
|
|
return light
|
|
end
|
|
|
|
-- Localise VoxelManip data buffer tables outside the loop,
|
|
-- to be re-used for all mapchunks, therefore minimising memory use.
|
|
local vm_data = {}
|
|
local vm_light_data = {}
|
|
|
|
-- Generate a piece of the map and set nodes in the specified area.
|
|
-- * minp: Minimum position of the area
|
|
-- * maxp: Maximum position of the area
|
|
-- * vm: VoxelManip object (only required when calling this function in the mapgen environment. Set to nil otherwise)
|
|
-- * prot_min: Minimum position of protected area. The protected area will NOT be overwritten by this function
|
|
-- * prot_min: Maximum position of protected area
|
|
local generate_piece = function(minp, maxp, vm, prot_min, prot_max)
|
|
-- Start time of mapchunk generation.
|
|
local t0 = os.clock()
|
|
|
|
if IS_IN_MAPGEN_ENVIRONMENT then
|
|
minetest.log("info", "[lzr_mapgen] Generating piece at " .. minetest.pos_to_string(minp).." ... (mapgen environment)")
|
|
else
|
|
minetest.log("info", "[lzr_mapgen] Generating piece at " .. minetest.pos_to_string(minp).." ... (main environment)")
|
|
end
|
|
|
|
local sidelen_x = maxp.x - minp.x + 1
|
|
local sidelen_z = maxp.z - minp.z + 1
|
|
local permapdims3d = {x = sidelen_x, y = sidelen_z}
|
|
|
|
local clear = IS_IN_MAPGEN_ENVIRONMENT ~= true
|
|
|
|
-- voxelmanip stuff
|
|
if not vm then
|
|
vm = minetest.get_voxel_manip(minp, maxp)
|
|
end
|
|
local emin, emax = vm:get_emerged_area()
|
|
|
|
local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
|
|
vm:get_data(vm_data)
|
|
vm:get_light_data(vm_light_data)
|
|
|
|
-- If we are fully outside the islands area,
|
|
-- we can switch to the ultra-fast deep ocean
|
|
-- algorithm.
|
|
local generate_islands = maxp.z > DEEP_OCEAN_Z
|
|
local isln_terrain = nil
|
|
local isln_var = nil
|
|
local isln_hills = nil
|
|
local isln_cliffs = nil
|
|
local isln_trees = nil
|
|
|
|
if generate_islands then
|
|
|
|
-- base terrain
|
|
local nobj_terrain = minetest.get_perlin_map(np_terrain, permapdims3d)
|
|
isln_terrain = nobj_terrain:get_2d_map({x=minp.x,y=minp.z})
|
|
|
|
-- base variation
|
|
local nobj_var = minetest.get_perlin_map(np_var, permapdims3d)
|
|
isln_var = nobj_var:get_2d_map({x=minp.x,y=minp.z})
|
|
|
|
-- hills
|
|
local nobj_hills = minetest.get_perlin_map(np_hills, permapdims3d)
|
|
isln_hills = nobj_hills:get_2d_map({x=minp.x+hills_offset,y=minp.z+hills_offset})
|
|
|
|
-- cliffs
|
|
local nobj_cliffs = minetest.get_perlin_map(np_cliffs, permapdims3d)
|
|
isln_cliffs = nobj_cliffs:get_2d_map({x=minp.x,y=minp.z})
|
|
|
|
-- trees
|
|
local nobj_trees = minetest.get_perlin_map(np_trees, permapdims3d)
|
|
isln_trees = nobj_trees:get_2d_map({x=minp.x,y=minp.z})
|
|
|
|
for z = 1, sidelen_z do
|
|
base_heightmap[z]={}
|
|
result_heightmap[z]={}
|
|
for x = 1, sidelen_x do
|
|
if not isln_terrain[z][x] then
|
|
minetest.log("error", "[lzr_mapgen] isln_terrain["..z.."]["..x.."] is nil!")
|
|
minetest.log("error", "[lzr_mapgen] LEN Z="..#isln_terrain)
|
|
for i=1, #isln_terrain do
|
|
minetest.log("error", "[lzr_mapgen] LEN X["..i.."]="..#isln_terrain[i])
|
|
end
|
|
minetest.log("error", "[lzr_mapgen] sidelen_x="..sidelen_x)
|
|
minetest.log("error", "[lzr_mapgen] sidelen_z="..sidelen_z)
|
|
return
|
|
end
|
|
if not isln_var[z][x] then
|
|
minetest.log("error", "[lzr_mapgen] isln_var["..z.."]["..x.."] is nil!")
|
|
minetest.log("error", "[lzr_mapgen] VAR LEN Z="..#isln_terrain)
|
|
minetest.log("error", "[lzr_mapgen] VAR LEN X="..#isln_terrain[1])
|
|
minetest.log("error", "[lzr_mapgen] sidelen_x="..sidelen_x)
|
|
minetest.log("error", "[lzr_mapgen] sidelen_z="..sidelen_z)
|
|
minetest.log("error", "[lzr_mapgen] isln_var="..dump(isln_var))
|
|
return
|
|
end
|
|
local theight = isln_terrain[z][x] + (convex and isln_var[z][x] or 0)
|
|
local hheight = isln_hills[z][x]
|
|
local cheight = isln_cliffs[z][x]
|
|
base_heightmap[z][x]=theight
|
|
result_heightmap[z][x]=get_terrain_height(theight,hheight,cheight)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
for z = minp.z, maxp.z do
|
|
for y = minp.y, maxp.y do
|
|
for x = minp.x, maxp.x do
|
|
local mpos = vector.new(x, y, z)
|
|
if not prot_min or not vector.in_area(mpos, prot_min, prot_max) then
|
|
local vi = area:index(x, y, z)
|
|
-- Deep ocean
|
|
if z <= DEEP_OCEAN_Z then
|
|
if y <= SEASTONE_LEVEL then
|
|
vm_data[vi] = c_stone
|
|
elseif y <= SEABED_LEVEL then
|
|
vm_data[vi] = c_seabed
|
|
elseif y <= WATER_LEVEL then
|
|
vm_data[vi] = c_water
|
|
-- We calculate the light level for the ocean manually
|
|
-- because calc_lighting apparently doesn't calculate
|
|
-- the lighting of the ocean.
|
|
-- FIXME: Remove the manual manual calculation entirely when we
|
|
-- figured out a way to fix ocean lighting with calc_lighting alone
|
|
vm_light_data[vi] = change_day_light(vm_light_data[vi], get_ocean_light_level(y))
|
|
elseif clear then
|
|
vm_data[vi] = minetest.CONTENT_AIR
|
|
end
|
|
-- Islands mapgen
|
|
else
|
|
local bheight = base_heightmap[z-minp.z+1][x-minp.x+1]
|
|
local theight = result_heightmap[z-minp.z+1][x-minp.x+1]
|
|
|
|
local dirt = (theight > 2 and theight < hills_thresh and isln_trees[z-minp.z+1][x-minp.x+1] > 0) and c_dirt_l or c_dirt_g
|
|
|
|
if theight > y then
|
|
vm_data[vi] = c_stone
|
|
elseif y==ceil(theight) then
|
|
vm_data[vi]= y < -3 and c_sand_dark or y<4 and c_sand or (y<60-random(3) and dirt or c_snow)
|
|
elseif y <= WATER_LEVEL then
|
|
vm_data[vi] = c_water
|
|
-- Calculate lighting manually (see above)
|
|
vm_light_data[vi] = change_day_light(vm_light_data[vi], get_ocean_light_level(y))
|
|
elseif clear then
|
|
vm_data[vi] = minetest.CONTENT_AIR
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
vm:set_data(vm_data)
|
|
vm:set_light_data(vm_light_data)
|
|
|
|
if IS_IN_MAPGEN_ENVIRONMENT then
|
|
if generate_islands then
|
|
minetest.generate_decorations(vm, minp, maxp)
|
|
end
|
|
vm:calc_lighting(minp, maxp)
|
|
else
|
|
if generate_islands then
|
|
minetest.generate_decorations(vm, minp, maxp)
|
|
end
|
|
vm:write_to_map(true)
|
|
end
|
|
|
|
-- Print generation time of this piece
|
|
local chugent = ceil((os.clock() - t0) * 1000)
|
|
if IS_IN_MAPGEN_ENVIRONMENT then
|
|
minetest.log("info", "[lzr_mapgen] Generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms (mapgen environment)")
|
|
else
|
|
minetest.log("info", "[lzr_mapgen] Generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms (main environment)")
|
|
end
|
|
end
|
|
|
|
|
|
if IS_IN_MAPGEN_ENVIRONMENT then
|
|
minetest.log("action", "[lzr_mapgen] mapgen script successfully run in mapgen environment")
|
|
-- On generated function for the mapgen environment
|
|
|
|
minetest.register_on_generated(function(vmanip, minp, maxp)
|
|
generate_piece(minp, maxp, vmanip)
|
|
end)
|
|
else
|
|
minetest.log("action", "[lzr_mapgen] mapgen script successfully run in global environment")
|
|
-- Expose generate_piece to the global environment when this file does not
|
|
-- run as mapgen thrad
|
|
lzr_mapgen.generate_piece = generate_piece
|
|
end
|