-- Average size of each cave biome local BIOME_SIZE = { x = 250, y = 250, z = 250 } -- Average step size of connectivity params -- Given that connectivity is mostly a horizontal feature, it mostly changes -- on a vertical scale, and barely on a horizontal scale. local CONNECTIVITY_BLOB = { x = 250, y = 100, z = 250 } local ENUM_AIR = 1 local ENUM_CEILING = 2 local ENUM_CEILING_DECORATION = 3 local ENUM_FLOOR = 4 local ENUM_FLOOR_DECORATION = 5 local ENUM_STONE = 6 local ENUM_WALL = 7 -- Point that is so out of the normal connectivity/verticality scope (0 - 100) -- that it is only selected when no other items are registered. local OUTLANDISH_POINT = -1e3 -- Average size of each cave shape local SHAPE_SIZE = { x = 128, y = 128, z = 128 } -- Several seeds for mapgen local SEED_CONNECTIVITY = 297948 local SEED_HEAT = 320523 local SEED_HUMIDITY = 9923473 local SEED_VERTICALITY = 35644 -- Average step size of verticality params -- Given that verticality is mostly a vertical feature, it mostly changes on a -- horizontal scale, and barely on a vertical scale. local VERTICALITY_BLOB = { x = 100, y = 250, z = 100 } -- Lower bound for cave generation local WORLD_MINP = { x = -1e6, y = -1e6, z = -1e6 } -- Upper bound for cave generation local WORLD_MAXP = { x = 1e6, y = 1e6, z = 1e6 } local WORLD_DEPTH = -60 local internal = {} ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -------------------------------- INTERNAL API --------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- function internal.cave_vastness(pos) local v = ns_caves.cave_vastness(pos) if not v then return 0 elseif v < 0 then return 0 elseif v > 1 then return 1 else return v end end -- Classify all nodes in the chunk into what they are -- TODO: Perhaps instead of iterating over ALL nodes, we can sort all node types -- TODO: into separate tables, effectively allowing us to process the nodes -- TODO: independently, saving computation time. function internal.classify_nodes(used_shapes, minp, maxp) local sminp = vector.offset(minp, 1, 1, 1) local smaxp = vector.offset(maxp, -1, -1, -1) local sva = VoxelArea(sminp, smaxp) local va = VoxelArea(minp, maxp) local items = {} for i in sva:iterp(sminp, smaxp) do local pos = sva:position(i) local bi = va:index(pos.x, pos.y, pos.z) local function is_part_of_cave(v) return used_shapes[va:index(v.x, v.y, v.z)] end if used_shapes[bi] == true then -- Part of cave if not used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then items[i] = ENUM_CEILING_DECORATION elseif not used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then items[i] = ENUM_FLOOR_DECORATION else items[i] = ENUM_AIR end else -- Not part of cave if used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then items[i] = ENUM_FLOOR elseif used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then items[i] = ENUM_CEILING elseif used_shapes[va:index(pos.x - 1, pos.y, pos.z)] then items[i] = ENUM_WALL elseif used_shapes[va:index(pos.x + 1, pos.y, pos.z)] then items[i] = ENUM_WALL elseif used_shapes[va:index(pos.x, pos.y, pos.z - 1)] then items[i] = ENUM_WALL elseif used_shapes[va:index(pos.x, pos.y, pos.z + 1)] then items[i] = ENUM_WALL else items[i] = ENUM_STONE end end end local custom_y = -13 if sminp.y < custom_y and custom_y < smaxp.y then for i in sva:iterp({ x = sminp.x, y = custom_y, z = sminp.z }, { x = smaxp.x, y = custom_y, z = smaxp.z }) do items[i] = ENUM_CEILING end end return items end function internal.clean_biome_def(def) assert(type(def) == "table") assert(type(def.name) == "string") assert(type(def.heat_point) == "number") assert(type(def.humidity_point) == "number") def.y_min = def.y_min or (def.min_pos and def.min_pos.y) or WORLD_MINP.y def.y_max = def.y_max or (def.max_pos and def.max_pos.y) or WORLD_MAXP.y def.min_pos = def.min_pos or { x = WORLD_MINP.x, y = def.y_min, z = WORLD_MINP.z } def.max_pos = def.max_pos or { x = WORLD_MAXP.x, y = def.y_max, z = WORLD_MAXP.z } assert(type(def.y_min) == "number") assert(type(def.y_max) == "number") assert(type(def.min_pos) == "table") assert(type(def.max_pos) == "table") assert(type(def.min_pos.x) == "number") assert(type(def.max_pos.x) == "number") assert(type(def.min_pos.y) == "number") assert(type(def.max_pos.y) == "number") assert(type(def.min_pos.z) == "number") assert(type(def.max_pos.z) == "number") def.node_air = def.node_air or "air" assert(def.node_dust == nil or type(def.node_dust) == "string") assert(def.node_floor == nil or type(def.node_floor) == "string") assert(def.node_wall == nil or type(def.node_wall) == "string") assert(def.node_roof == nil or type(def.node_roof) == "string") assert(type(def.node_air) == "string") return def end function internal.clean_shape_def(def) assert( type(def) == "table", "Shape def is meant to be a table type" ) assert(type(def.name) == "string", "Shape name is meant to be a string") assert(type(def.connectivity_point) == "number") assert(type(def.verticality_point) == "number") def.y_min = def.y_min or WORLD_MINP.y def.y_max = def.y_max or WORLD_MAXP.y def.func = def.func or function(_, n) return n end assert(type(def.y_min) == "number") assert(type(def.y_max) == "number") assert(type(def.func) == "function") assert(def.noise_params == nil or type(def.noise_params) == "table") return def end -- Get connectivity noise params function internal.connectivity_noise_params() local factor = math.max(1, math.abs(#ns_caves.registered_shapes) ^ 0.5) return { offset = 50, scale = 50, spread = { x = factor * CONNECTIVITY_BLOB.x, y = factor * CONNECTIVITY_BLOB.y, z = factor * CONNECTIVITY_BLOB.z, }, seed = SEED_CONNECTIVITY, octaves = 2, persistence = 0.2, lacunarity = 2.0, flags = "eased" } end -- Get a default cave biome in case no biomes are registered function internal.default_biome() return internal.clean_biome_def( { name = "noordstar_caves:default_biome" , heat_point = OUTLANDISH_POINT , humidity_point = OUTLANDISH_POINT } ) end -- Get a default cave shape in case no shapes are registered function internal.default_shape() return internal.clean_shape_def( { name = "noordstar_caves:none" , connectivity_point = OUTLANDISH_POINT , verticality_point = OUTLANDISH_POINT , func = function (pos, v) return 0 end } ) end -- Get a (sorta) Euclidian distance. The exact distance doesn't matter, -- it just needs to correctly determine whichever value is closer in a -- Euclidian space. Consequently, the square root is ignored as optimization. function internal.euclidian(x1, x2, y1, y2) return (x1 - x2) ^ 2 + (y1 - y2) ^ 2 end -- For each node, determine which cave biome they're in. function internal.find_biome_allocations(heat, humidity) assert(#heat == #humidity) local allocs = {} local default_biome = internal.default_biome() for i = 1, #heat, 1 do local e, u = heat[i], humidity[i] local d = internal.euclidian( e, default_biome.heat_point, u, default_biome.humidity_point ) local biome_name -- Find the appropriate biome for name, def in pairs(ns_caves.registered_biomes) do local def_d = internal.euclidian( e, def.heat_point, u, def.humidity_point ) if def_d < d then d = def_d biome_name = name end end allocs[i] = biome_name end return allocs end -- For each node, determine which cave shape they follow. function internal.find_shape_allocations(connectivity, verticality) assert(#connectivity == #verticality) local allocs = {} local default_shape = internal.default_shape() for i = 1, #connectivity, 1 do local c, v = connectivity[i], verticality[i] local d = internal.euclidian( c, default_shape.connectivity_point, v, default_shape.verticality_point ) local shape_name -- Find the appropriate shape for name, def in pairs(ns_caves.registered_shapes) do local def_d = internal.euclidian( c, def.connectivity_point, v, def.verticality_point ) if def_d < d then d = def_d shape_name = name end end -- Assign the chosen name allocs[i] = shape_name end return allocs end -- Once it has been figured out which node belongs to which shape, -- get noise values from the respective shapes. -- This function does its operations in-place. function internal.find_shape_values(used_shapes, minp, maxp, va) -- Cache shapes so they don't need to be recalculated. local captured_shapes = {} local default_shape = internal.default_shape() for i in va:iterp(minp, maxp) do -- Get shape values per item local name = used_shapes[i] local shape if name == nil then shape = default_shape else shape = ns_caves.registered_shapes[name] end if captured_shapes[shape] == nil then -- minetest.debug("Generating new shape! " .. shape.name) captured_shapes[shape] = internal.generate_shape(shape, minp, maxp, va) end used_shapes[i] = captured_shapes[shape][i] if used_shapes[i] == nil then error( "Noise value ended up nil unexpectedly: i = " .. i .. ", shape = " .. shape.name ) end end -- minetest.debug("Finished generating shapes.") end function internal.generate_connectivity_noise(minp, maxp) return internal.generate_perlin_noise( internal.connectivity_noise_params(), minp, maxp ) end function internal.generate_heat_noise(minp, maxp) return internal.generate_perlin_noise( internal.heat_noise_params(), minp, maxp ) end function internal.generate_humidity_noise(minp, maxp) return internal.generate_perlin_noise( internal.humidity_noise_params(), minp, maxp ) end -- Generate Perlin noise within given boundaries function internal.generate_perlin_noise(noiseparams, minp, maxp) local size = { x = 1 + maxp.x - minp.x, y = 1 + maxp.y - minp.y, z = 1 + maxp.z - minp.z, } return PerlinNoiseMap(noiseparams, size):get_3d_map_flat(minp) end function internal.generate_shape(def, minp, maxp, va) -- local noise_flat = {} -- Get random noise if noise_params are given if def.noise_params then return internal.generate_perlin_noise(def.noise_params, minp, maxp) else return {} -- for i in va:iterp(minp, maxp) do -- noise_flat[i] = 0 -- end end -- -- Update noise with custom defined function -- for i in va:iterp(minp, maxp) do -- noise_flat[i] = def.func(va:position(i), noise_flat[i]) -- end -- return noise_flat end -- Generate verticality noise within given boundaries function internal.generate_verticality_noise(minp, maxp) return internal.generate_perlin_noise( internal.verticality_noise_params(), minp, maxp ) end -- Get the noise params for the cave biome temperature. function internal.heat_noise_params() return { offset = 50, scale = 50, spread = BIOME_SIZE, seed = SEED_HEAT, octaves = 2, persistence = 0.1, lacunarity = 2.0, flags = "" } end -- Get the noise params for the cave biome humidity. function internal.humidity_noise_params() return { offset = 50, scale = 50, spread = BIOME_SIZE, seed = SEED_HUMIDITY, octaves = 2, persistence = 0.1, lacunarity = 2.0, flags = "" } end -- Take all necessary steps to execute the mapgen function internal.mapgen(minp, maxp, blockseed, vm, va) -- Create bordered VoxelArea. -- The point of this is so walls and ceilings can be determined using -- bordering nodes in different chunks. local bminp = vector.offset(minp, -1, -1, -1) local bmaxp = vector.offset(maxp, 1, 1, 1) local bva = VoxelArea(bminp, bmaxp) -- Find cave shape params local connectivity = internal.generate_connectivity_noise(bminp, bmaxp) local verticality = internal.generate_verticality_noise(bminp, bmaxp) -- Draw cave shapes local used_shapes = internal.find_shape_allocations(connectivity, verticality) internal.find_shape_values(used_shapes, bminp, bmaxp, bva) internal.shape_to_air(used_shapes, bminp, bmaxp, bva) -- Find cave biome params local heat = internal.generate_heat_noise(minp, maxp) local humidity = internal.generate_humidity_noise(minp, maxp) -- -- DEBUG: Write to air (or not) -- local air = 0 -- for i in small_va:iterp(minp, maxp) do -- if used_shapes[i] == true then -- local pos = small_va:position(i) -- local vmi = va:index(pos.x, pos.y, pos.z) -- data[vmi] = minetest.CONTENT_AIR -- air = air + 1 -- end -- end -- Classify various nodes as walls, floors, ceilings local classified_nodes = internal.classify_nodes(used_shapes, bminp, bmaxp) -- Draw cave biomes local used_biomes = internal.find_biome_allocations(heat, humidity) -- Manipulate `data` table by adding classified nodes based on which biome -- they're in. local data = vm:get_data() internal.write_classified_biome_nodes( data, va, classified_nodes, used_biomes, minp, maxp ) vm:set_data(data) vm:write_to_map() end -- Place items function internal.place_node_on_data(data, i, name) if name == nil then return end local node_id = minetest.get_content_id(name) if node_id == nil then return end data[i] = node_id end -- Register a new biome function internal.register_biome(biome) biome = internal.clean_biome_def(biome) ns_caves.registered_biomes[biome.name] = biome end -- Register a new shape function internal.register_shape(shape) shape = internal.clean_shape_def(shape) ns_caves.registered_shapes[shape.name] = shape end -- Convert all shape noise into clarifications whether a node is wall or air. -- This function does its operations in-place. function internal.shape_to_air(noise_values, minp, maxp, va) for i in va:iterp(minp, maxp) do local vastness = internal.cave_vastness(va:position(i)) if noise_values[i] == nil then error( "Noise value ended up nil unexpectedly" ) elseif noise_values[i] >= 1 - vastness then noise_values[i] = true else noise_values[i] = false end end end -- Get verticality noise params function internal.verticality_noise_params() local factor = math.max(1, math.abs(#ns_caves.registered_shapes) ^ 0.5) return { offset = 50, scale = 50, spread = { x = factor * VERTICALITY_BLOB.x, y = factor * VERTICALITY_BLOB.y, z = factor * VERTICALITY_BLOB.z, }, seed = SEED_VERTICALITY, octaves = 2, persistence = 0.2, lacunarity = 2.0, flags = "eased" } end function internal.write_classified_biome_nodes(vm_data, va, classified_nodes, used_biomes, minp, maxp) local small_va = VoxelArea(minp, maxp) -- assert(#classified_nodes == #used_biomes) -- assert(#classified_nodes == small_va:getVolume()) local default_biome = internal.default_biome() local schems = {} for i in va:iterp(minp, maxp) do local pos = va:position(i) local si = small_va:index(pos.x, pos.y, pos.z) local node_type = classified_nodes[si] local biome = used_biomes[si] if biome == nil then biome = default_biome else biome = ns_caves.registered_biomes[biome] or default_biome end if node_type == ENUM_AIR then internal.place_node_on_data(vm_data, i, biome.node_air) elseif node_type == ENUM_CEILING then internal.place_node_on_data(vm_data, i, biome.node_roof) elseif node_type == ENUM_CEILING_DECORATION then -- TODO: Return schematics to be placed internal.place_node_on_data(vm_data, i, biome.node_air) elseif node_type == ENUM_FLOOR then internal.place_node_on_data(vm_data, i, biome.node_floor) elseif node_type == ENUM_FLOOR_DECORATION then -- TODO: Return schematics to be placed internal.place_node_on_data(vm_data, i, biome.node_air) elseif node_type == ENUM_STONE then -- Nothing needs to be placed elseif node_type == ENUM_WALL then internal.place_node_on_data(vm_data, i, biome.node_wall) else if node_type == nil then error( "Expected enum value, encountered nil at index " .. si .. " (originally " .. i .. ")" ) end error( "Encountered unknown node type enum value " .. node_type ) end end return schems end ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- --------------------------------- PUBLIC API ---------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- minetest.register_on_generated(function(minp, maxp, blockseed) if maxp.y < WORLD_DEPTH then return end local vm = minetest.get_mapgen_object("voxelmanip") local va = VoxelArea(vm:get_emerged_area()) internal.mapgen(minp, maxp, blockseed, vm, va) end) ns_caves = { cave_vastness = function(pos) if pos.y > 0 or pos.y < WORLD_DEPTH then return 0 end local y = math.abs(pos.y) local d = math.abs(WORLD_DEPTH / 2) return 1 - (math.abs(y - d) / d) end, register_biome = internal.register_biome, register_shape = internal.register_shape, registered_biomes = {}, registered_shapes = {}, } dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/lua/register.lua")