feat(core): implement surface terrain generation from noise heightmaps

This commit is contained in:
dualfroz committed 2026-08-23 19:10:39 +02:00
1 parent 22051db61e
commit 036dae1955
1 file changed
+234
+234
View File
@@ -0,0 +1,234 @@
#include "vf/WorldGen.h"
#include <cmath>
#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<int>(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<int>(h % 12u); // interior anchor, footprint stays in column
int lz = 2 + static_cast<int>((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<StructureEdit> edits = buildStructure(kind, structSeed);
stampStructure(world, {gx, surface + 1, gz}, edits, params.maxY);
}
} // namespace vf