OpenMiner/source/server/world/TerrainGenerator.cpp

277 lines
9.9 KiB
C++

/*
* =====================================================================================
*
* OpenMiner
*
* Copyright (C) 2018-2020 Unarelith, Quentin Bazin <openminer@unarelith.net>
* Copyright (C) 2019-2020 the OpenMiner contributors (see CONTRIBUTORS.md)
*
* This file is part of OpenMiner.
*
* OpenMiner is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* OpenMiner is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with OpenMiner; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* =====================================================================================
*/
#include "EngineConfig.hpp"
#include "Registry.hpp"
#include "ServerChunk.hpp"
#include "TerrainGenerator.hpp"
#include "World.hpp"
#include <glm/gtc/noise.hpp>
#include "FastNoise.hpp"
void TerrainGenerator::generate(ServerChunk &chunk) const {
fastNoiseGeneration(chunk);
}
void TerrainGenerator::fastNoiseGeneration(ServerChunk &chunk) const {
FastNoise noise;
noise.SetNoiseType(FastNoise::NoiseType::SimplexFractal);
noise.SetFrequency(1 / 256.0f);
noise.SetFractalOctaves(4);
Random_t rand;
rand.seed(chunk.x() + chunk.y() * CHUNK_WIDTH + chunk.z() * CHUNK_WIDTH * CHUNK_HEIGHT + 1337);
Chunk *topChunk = chunk.getSurroundingChunk(Chunk::Top);
for(int y = 0 ; y < CHUNK_DEPTH ; y++) {
for(int x = 0 ; x < CHUNK_WIDTH ; x++) {
u16 biomeIndex = m_biomeSampler.getBiomeIndexAt(x + chunk.x() * CHUNK_WIDTH, y + chunk.y() * CHUNK_DEPTH);
const Biome &biome = Registry::getInstance().getBiome(biomeIndex);
// Land height
double n = noise.GetNoise(-x - chunk.x() * CHUNK_WIDTH, y + chunk.y() * CHUNK_DEPTH);
double h = 10 + n * 20;
// double n = noise2d((x + chunk.x() * CHUNK_WIDTH) / 256.0, (y + chunk.y() * CHUNK_DEPTH) / 256.0, 4, 0.5) * 4;
// double h = 10 + n * 2;
// Land blocks
for(int z = 0 ; z < CHUNK_HEIGHT ; z++) {
// Are we above "ground" level?
if(z + chunk.z() * CHUNK_HEIGHT > h) {
// If we are not yet up to sea level, fill with water blocks
if (z + chunk.z() * CHUNK_HEIGHT < SEALEVEL) {
chunk.setBlockRaw(x, y, z, biome.getLiquidBlockID());
}
// Otherwise we are in the air
else {
// Try to place a tree
bool placedTree = tryPlaceTree(chunk, x, y, z, biome, rand);
// Otherwise try to place flora.
bool placedFlora = false;
if (!placedTree)
tryPlaceFlora(chunk, x, y, z, biome, rand);
// Or a portal
bool placedPortal = false;
if (!placedTree && !placedFlora)
tryPlacePortal(chunk, x, y, z, biome, rand);
// Otherwise set sunlight.
if (!placedTree && !placedFlora && !placedPortal && z == CHUNK_HEIGHT - 1) {
chunk.lightmap().addSunlight(x, y, z, 15);
}
}
}
else {
if (z + chunk.z() * CHUNK_HEIGHT >= h - 1 && z + chunk.z() * CHUNK_HEIGHT > SEALEVEL - 1)
chunk.setBlockRaw(x, y, z, biome.getTopBlockID());
else if (z + chunk.z() * CHUNK_HEIGHT <= SEALEVEL - 1 && h < SEALEVEL && z + chunk.z() * CHUNK_HEIGHT > h - 3)
chunk.setBlockRaw(x, y, z, biome.getBeachBlockID());
else if (z + chunk.z() * CHUNK_HEIGHT > h - 3)
chunk.setBlockRaw(x, y, z, biome.getGroundBlockID());
else
chunk.setBlockRaw(x, y, z, biome.getDeepBlockID());
// Populate ores.
generateOres(chunk, x, y, z, biome, rand);
// Caves
generateCaves(chunk, x, y, z, h);
}
if (topChunk && topChunk->isInitialized()) {
int sunlightLevel = topChunk->lightmap().getSunlight(x, y, 0);
if (sunlightLevel) {
chunk.lightmap().addSunlight(x, y, CHUNK_HEIGHT - 1, sunlightLevel);
}
}
}
}
}
}
inline bool TerrainGenerator::tryPlaceTree(ServerChunk &chunk, int x, int y, int z, const Biome &biome, Random_t &rand) const {
if (chunk.getBlock(x, y, z - 1) == biome.getTopBlockID()) {
for (const PlacementEntry::Tree &treePlacement : biome.getTrees()) {
if (!rand.get<bool>(treePlacement.probability))
continue;
const Tree &tree = Registry::getInstance().getTree(treePlacement.treeID);
// Trunk
int h = rand.get(tree.trunkMinHeight(), tree.trunkMaxHeight());
for (int i = 0; i < h; i++) {
chunk.setBlockRaw(x, y, z + i, tree.getLogBlockID());
}
// Leaves
if (tree.hasLeaves()) {
for (int iz = -3; iz <= 3; iz++) {
for (int iy = -3; iy <= 3; iy++) {
for (int ix = -3; ix <= 3; ix++) {
u16 block = chunk.getBlock(x + ix, y + iy, z + h + iz);
if (ix * ix + iy * iy + iz * iz < 8 + rand.get(0, 1) && !block) {
chunk.setBlockRaw(x + ix, y + iy, z + h + iz, tree.getLeavesBlockID());
// FIXME: This is a temporary fix for the second part of #41
chunk.lightmap().setSunlight(x + ix, y + iy, z + h + iz, 0);
}
}
}
}
}
return true;
}
}
return false;
}
inline bool TerrainGenerator::tryPlaceFlora(ServerChunk &chunk, int x, int y, int z, const Biome &biome, Random_t &rand) const {
for (const PlacementEntry::Flora &flora : biome.getFlora()) {
if (chunk.getBlock(x, y, z - 1) != flora.spawnsOnBlockID)
continue;
if (!rand.get<bool>(flora.probability))
continue;
chunk.setBlockRaw(x, y, z, flora.blockID);
return true;
}
return false;
}
inline bool TerrainGenerator::tryPlacePortal(ServerChunk &chunk, int x, int y, int z, const Biome &biome, Random_t &rand) const {
// FIXME: This is a temporary portal generation
// This code should be replaced by a proper "feature" implementation
// which will also allow making stuff like villages easier
if (chunk.getBlock(x, y, z - 1) == biome.getTopBlockID() && rand.get<bool>(0.0002)) {
for (int ix = 0 ; ix < 4 ; ++ix) {
for (int iz = 0 ; iz < 5 ; ++iz) {
if (ix == 0 || iz == 0 || ix == 3 || iz == 4)
chunk.setBlockRaw(x + ix, y, z + iz, biome.getPortalFrameBlockID());
else
chunk.setBlockRaw(x + ix, y, z + iz, biome.getPortalBlockID());
}
}
return true;
}
return false;
}
inline void TerrainGenerator::generateOres(ServerChunk &chunk, int x, int y, int z, const Biome &biome, Random_t &rand) const {
// TODO: Like trees, ores should be able to seamlessly cross chunk boundaries.
// This could be achieved either by setting up a generation pipeline with stages,
// processing neighboring chunks' ores every time, or generating them with noise.
for (const PlacementEntry::Ore &ore : biome.getOres()) {
if (!rand.get<bool>(ore.probability))
continue;
oreFloodFill(chunk, x, y, z, biome.getDeepBlockID(), ore.blockID, 2, rand);
break;
}
}
inline void TerrainGenerator::generateCaves(ServerChunk &chunk, int x, int y, int z, int h) const {
float n2 = noise2d(-(x + chunk.x() * CHUNK_WIDTH) / 256.0, (y + chunk.y() * CHUNK_DEPTH) / 256.0, 8, 0.3) * 4;
float r2 = noise3d_abs(-(x + chunk.x() * CHUNK_WIDTH) / 512.0f, (z + chunk.z() * CHUNK_HEIGHT) / 512.0f, (y + chunk.y() * CHUNK_DEPTH) / 512.0f, 4, 0.1);
float r3 = noise3d_abs(-(x + chunk.x() * CHUNK_WIDTH) / 512.0f, (z + chunk.z() * CHUNK_HEIGHT) / 128.0f, (y + chunk.y() * CHUNK_DEPTH) / 512.0f, 4, 1);
float r4 = n2 * 5 + r2 * r3 * 20;
if (r4 > 6 && r4 < 8 && h > SEALEVEL) {
chunk.setBlockRaw(x, y, z - 1, 0);
chunk.setBlockRaw(x, y, z, 0);
chunk.setBlockRaw(x, y, z + 1, 0);
}
}
void TerrainGenerator::oreFloodFill(ServerChunk &chunk, double x, double y, double z, u16 toReplace, u16 replaceWith, int depth, Random_t &rand) const {
if (depth < 0) return;
if (chunk.getBlock(x, y, z) == replaceWith) return;
if (chunk.getBlock(x, y, z) == toReplace)
chunk.setBlockRaw(x, y, z, replaceWith);
oreFloodFill(chunk, x + 1, y, z, toReplace, replaceWith, depth - 1, rand);
oreFloodFill(chunk, x - 1, y, z, toReplace, replaceWith, depth - 1, rand);
oreFloodFill(chunk, x, y + 1, z, toReplace, replaceWith, depth - 1, rand);
oreFloodFill(chunk, x, y - 1, z, toReplace, replaceWith, depth - 1, rand);
oreFloodFill(chunk, x, y, z + 1, toReplace, replaceWith, depth - 1, rand);
oreFloodFill(chunk, x, y, z - 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x + 1, y + 1, z + 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x + 1, y + 1, z - 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x + 1, y - 1, z + 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x + 1, y - 1, z - 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x - 1, y + 1, z + 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x - 1, y + 1, z - 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x - 1, y - 1, z + 1, toReplace, replaceWith, depth - 1, rand);
if (rand.get<bool>(1.f / 15.f))
oreFloodFill(chunk, x - 1, y - 1, z - 1, toReplace, replaceWith, depth - 1, rand);
}
inline float TerrainGenerator::noise2d(double x, double y, int octaves, float persistence) {
float sum = 0;
float strength = 1.0;
float scale = 1.0;
for(int i = 0 ; i < octaves ; i++) {
sum += strength * glm::simplex(glm::vec2{x, y} * scale);
scale *= 2.0;
strength *= persistence;
}
return sum;
}
inline float TerrainGenerator::noise3d_abs(double x, double y, double z, int octaves, float persistence) {
float sum = 0;
float strength = 1.0;
float scale = 1.0;
for(int i = 0 ; i < octaves ; i++) {
sum += strength * fabsf(glm::simplex(glm::vec3{x, y, z} * scale));
scale *= 2.0;
strength *= persistence;
}
return sum;
}