From 036dae19557dc7e7ce275765c24552efc6c7896d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 19:10:39 +0200 Subject: [PATCH] feat(core): implement surface terrain generation from noise heightmaps --- core/src/WorldGen.cpp | 234 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 core/src/WorldGen.cpp diff --git a/core/src/WorldGen.cpp b/core/src/WorldGen.cpp new file mode 100644 index 0000000..bc71d88 --- /dev/null +++ b/core/src/WorldGen.cpp @@ -0,0 +1,234 @@ +#include "vf/WorldGen.h" + +#include + +#include "vf/Structures.h" + +namespace vf { + +namespace { + +struct BiomeShape { + int base; + float amplitude; +}; + +BiomeShape shapeFor(Biome biome) { + switch (biome) { + case Biome::Mountains: return {40, 46.0f}; + case Biome::Snow: return {32, 22.0f}; + case Biome::Desert: return {28, 8.0f}; + case Biome::Forest: return {30, 14.0f}; + default: return {30, 11.0f}; + } +} + +void placeTree(World& world, int gx, int baseY, int gz, int maxY, bool tall) { + int trunk = tall ? 6 : 4; + int top = baseY + trunk; + for (int y = baseY; y <= top && y < maxY; ++y) world.setBlock(gx, y, gz, Block::Wood); + for (int dx = -2; dx <= 2; ++dx) { + for (int dz = -2; dz <= 2; ++dz) { + for (int dy = 0; dy <= 2; ++dy) { + int radius = 2 - dy; + if (std::abs(dx) + std::abs(dz) > radius + 1) continue; + int ly = top - 1 + dy; + if (ly >= maxY) continue; + if (dx == 0 && dz == 0 && ly <= top) continue; + if (world.blockAt(gx + dx, ly, gz + dz) == Block::Air) + world.setBlock(gx + dx, ly, gz + dz, Block::Leaves); + } + } + } +} + +} // namespace + +WorldGen::WorldGen(const WorldGenParams& params) + : params(params), + heightNoise(params.seed), + tempNoise(params.seed ^ 0x9e3779b9u), + humidNoise(params.seed ^ 0x85ebca6bu), + mountainNoise(params.seed ^ 0xc2b2ae35u), + caveNoise(params.seed ^ 0x27d4eb2fu), + oreNoise(params.seed ^ 0x165667b1u), + treeNoise(params.seed ^ 0xd3a2646cu) {} + +Biome WorldGen::climateBiome(int gx, int gz) const { + float temp = tempNoise.noise2D(gx * params.biomeFrequency, gz * params.biomeFrequency); + float humid = humidNoise.noise2D(gx * params.biomeFrequency * 1.3f, + gz * params.biomeFrequency * 1.3f); + float mountain = mountainNoise.noise2D(gx * params.biomeFrequency * 0.7f, + gz * params.biomeFrequency * 0.7f); + if (mountain > 0.55f) return Biome::Mountains; + if (temp < -0.35f) return Biome::Snow; + if (temp > 0.4f && humid < -0.1f) return Biome::Desert; + if (humid > 0.2f) return Biome::Forest; + return Biome::Plains; +} + +int WorldGen::surfaceHeight(int gx, int gz) const { + BiomeShape shape = shapeFor(climateBiome(gx, gz)); + float n = heightNoise.fbm2D(gx * params.heightFrequency, gz * params.heightFrequency, + params.heightOctaves, 2.0f, 0.5f); + float ridged = 1.0f - std::fabs(n); + float shaped = n * 0.7f + (ridged - 0.5f) * 0.6f; + int h = shape.base + static_cast(std::round(shaped * shape.amplitude)); + if (h < 1) h = 1; + if (h > params.maxY - 1) h = params.maxY - 1; + return h; +} + +Biome WorldGen::biomeAt(int gx, int gz) const { + int h = surfaceHeight(gx, gz); + if (h < params.seaLevel - 1) return Biome::Ocean; + if (h <= params.seaLevel + 1) return Biome::Beach; + return climateBiome(gx, gz); +} + +bool WorldGen::isCave(int gx, int gy, int gz) const { + float f = params.caveFrequency; + float c1 = 1.0f - std::fabs(caveNoise.noise3D(gx * f, gy * f * 2.0f, gz * f)); + float c2 = 1.0f - std::fabs(caveNoise.noise3D(gx * f + 31.7f, gy * f * 2.0f + 13.1f, + gz * f - 27.3f)); + return c1 > params.caveThreshold && c2 > params.caveThreshold; +} + +Block WorldGen::topBlock(Biome biome, int height) const { + switch (biome) { + case Biome::Ocean: + case Biome::Beach: + case Biome::Desert: + return Block::Sand; + case Biome::Snow: + return Block::Snow; + case Biome::Mountains: + return height > 42 ? Block::Snow : Block::Grass; + default: + return Block::Grass; + } +} + +void WorldGen::generateChunk(Chunk& chunk, const Vec3i& chunkCoord) const { + int ox = chunkCoord.x * chunkSize; + int oy = chunkCoord.y * chunkSize; + int oz = chunkCoord.z * chunkSize; + + for (int lx = 0; lx < chunkSize; ++lx) { + for (int lz = 0; lz < chunkSize; ++lz) { + int gx = ox + lx; + int gz = oz + lz; + Biome biome = biomeAt(gx, gz); + int h = surfaceHeight(gx, gz); + bool sandy = biome == Biome::Beach || biome == Biome::Ocean || biome == Biome::Desert; + + for (int ly = 0; ly < chunkSize; ++ly) { + int gy = oy + ly; + Block block = Block::Air; + + if (gy == 0) { + block = Block::Stone; + } else if (gy > h) { + block = gy <= params.seaLevel ? Block::Water : Block::Air; + } else if (gy == h) { + block = (h <= params.seaLevel) ? (sandy ? Block::Sand : Block::Dirt) + : topBlock(biome, h); + } else if (gy >= h - 3) { + block = sandy ? Block::Sand : Block::Dirt; + } else { + block = Block::Stone; + } + + if (block == Block::Stone && gy < h - 3 && gy >= 1) { + float o = oreNoise.noise3D(gx * params.oreFrequency, gy * params.oreFrequency, + gz * params.oreFrequency); + if (o > 0.72f) { + if (gy < 14) block = Block::GoldOre; + else if (gy < 40) block = Block::IronOre; + else block = Block::CoalOre; + } else if (o < -0.78f) { + block = Block::Gravel; + } + } + + if (gy >= 2 && gy < h - 2 && isSolid(block) && isCave(gx, gy, gz)) + block = Block::Air; + + chunk.set(lx, ly, lz, block); + } + } + } +} + +void WorldGen::generateColumn(World& world, int columnX, int columnZ) const { + for (int cy = 0; cy < stackChunkCount(); ++cy) { + Chunk& chunk = world.ensureChunk({columnX, cy, columnZ}); + generateChunk(chunk, {columnX, cy, columnZ}); + } + + if (!params.trees) return; + int ox = columnX * chunkSize; + int oz = columnZ * chunkSize; + for (int lx = 2; lx < chunkSize - 2; ++lx) { + for (int lz = 2; lz < chunkSize - 2; ++lz) { + int gx = ox + lx; + int gz = oz + lz; + Biome biome = biomeAt(gx, gz); + if (biome != Biome::Forest && biome != Biome::Plains) continue; + int h = surfaceHeight(gx, gz); + if (h <= params.seaLevel + 1 || h >= params.maxY - 10) continue; + if (world.blockAt(gx, h, gz) != Block::Grass) continue; + float density = biome == Biome::Forest ? 0.55f : 0.68f; + float t = treeNoise.noise2D(gx * 0.87f + 11.3f, gz * 0.87f - 7.1f); + if (t > density) placeTree(world, gx, h + 1, gz, params.maxY, biome == Biome::Forest); + } + } + + placeStructures(world, columnX, columnZ); +} + +void WorldGen::placeStructures(World& world, int columnX, int columnZ) const { + if (!params.structures) return; + + uint32_t h = structureHash(params.seed ^ 0x51ed270bu, columnX, 0, columnZ); + int lx = 2 + static_cast(h % 12u); // interior anchor, footprint stays in column + int lz = 2 + static_cast((h >> 8) % 12u); + int gx = columnX * chunkSize + lx; + int gz = columnZ * chunkSize + lz; + + int surface = surfaceHeight(gx, gz); + if (surface <= params.seaLevel + 1 || surface >= params.maxY - 14) return; + + Block top = world.blockAt(gx, surface, gz); + if (top != Block::Grass && top != Block::Snow) return; + + Biome biome = biomeAt(gx, gz); + uint32_t roll = (h >> 16) % 100u; + StructureKind kind; + bool place = false; + + if (biome == Biome::Forest || biome == Biome::Plains) { + if (roll < 5) { + kind = StructureKind::Hut; + place = true; + } else if (roll < 45) { + kind = StructureKind::OakTree; + place = true; + } + } else if (biome == Biome::Snow || biome == Biome::Mountains) { + if (roll < 4) { + kind = StructureKind::Hut; + place = true; + } else if (roll < 35) { + kind = StructureKind::PineTree; + place = true; + } + } + if (!place) return; + + uint32_t structSeed = structureHash(params.seed, columnX, 1, columnZ); + std::vector edits = buildStructure(kind, structSeed); + stampStructure(world, {gx, surface + 1, gz}, edits, params.maxY); +} + +} // namespace vf