diff --git a/core/include/vf/BiomeGen.h b/core/include/vf/BiomeGen.h new file mode 100644 index 0000000..7547871 --- /dev/null +++ b/core/include/vf/BiomeGen.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include "Block.h" +#include "NoiseAdv.h" + +namespace vf { + +// Standalone climate-driven biome catalogue. Deliberately named BiomeKind, +// distinct from vf::Biome in WorldGen.h, so this header can be included +// alongside WorldGen.h without redefinition. +enum class BiomeKind : uint8_t { + Ocean = 0, + Beach = 1, + Desert = 2, + Plains = 3, + Forest = 4, + Taiga = 5, + Tundra = 6, + Swamp = 7, + Jungle = 8, + Savanna = 9, + Mountains = 10, + Count, +}; + +const char* biomeKindName(BiomeKind biome); + +// Five-axis climate sample at a world column, each roughly in [-1, 1]. +struct ClimateSample { + float temperature = 0.0f; + float humidity = 0.0f; + float continentalness = 0.0f; // low = ocean, high = deep inland + float erosion = 0.0f; // low = jagged/mountainous, high = flat + float weirdness = 0.0f; // near 0 carves a river valley +}; + +struct BiomeGenParams { + uint32_t seed = 1337u; + float climateFrequency = 0.003f; + float erosionFrequency = 0.006f; + float weirdnessFrequency = 0.01f; + float riverWidth = 0.045f; + int seaLevel = 26; +}; + +struct SurfaceLayers { + Block top = Block::Grass; + Block filler = Block::Dirt; + int fillerDepth = 3; +}; + +struct DecoratorDensity { + float treeDensity = 0.0f; + float bushDensity = 0.0f; + float caneDensity = 0.0f; // cactus in desert, reeds in swamp/jungle +}; + +// Vote-blended biome weights over a neighbourhood, used to smooth borders +// (surface colour, height blending) instead of a hard biome edge. +struct BiomeWeights { + std::array(BiomeKind::Count)> weight{}; + + float weightOf(BiomeKind biome) const { return weight[static_cast(biome)]; } +}; + +// Climate-parameter biome classifier with river carving, surface builders +// and decorator density tables. Independent of vf::WorldGen: samples are +// pure functions of world column coordinates, so identical seed and column +// always yield an identical result. +class BiomeGen { +public: + explicit BiomeGen(const BiomeGenParams& params = {}); + + ClimateSample climateAt(int gx, int gz) const; + BiomeKind biomeFromClimate(const ClimateSample& climate) const; + BiomeKind biomeAt(int gx, int gz) const; + + bool isRiver(int gx, int gz) const; + + SurfaceLayers surfaceFor(BiomeKind biome) const; + + // Surface block at height `height` for the column at (gx, gz): river + // water/sand overrides the biome's ordinary top block. + Block surfaceBlockAt(int gx, int gz, int height) const; + + DecoratorDensity decoratorsFor(BiomeKind biome) const; + + // Box-blurred biome vote over a (2*radius+1)^2 neighbourhood, weights + // sum to 1. + BiomeWeights biomeBlend(int gx, int gz, int radius) const; + + const BiomeGenParams& settings() const { return params; } + +private: + BiomeGenParams params; + NoiseAdv temperatureNoise; + NoiseAdv humidityNoise; + NoiseAdv continentalnessNoise; + NoiseAdv erosionNoise; + NoiseAdv weirdnessNoise; +}; + +} // namespace vf diff --git a/core/src/BiomeGen.cpp b/core/src/BiomeGen.cpp new file mode 100644 index 0000000..1367d09 --- /dev/null +++ b/core/src/BiomeGen.cpp @@ -0,0 +1,140 @@ +#include "vf/BiomeGen.h" + +#include + +namespace vf { + +const char* biomeKindName(BiomeKind biome) { + switch (biome) { + case BiomeKind::Ocean: return "Ocean"; + case BiomeKind::Beach: return "Beach"; + case BiomeKind::Desert: return "Desert"; + case BiomeKind::Plains: return "Plains"; + case BiomeKind::Forest: return "Forest"; + case BiomeKind::Taiga: return "Taiga"; + case BiomeKind::Tundra: return "Tundra"; + case BiomeKind::Swamp: return "Swamp"; + case BiomeKind::Jungle: return "Jungle"; + case BiomeKind::Savanna: return "Savanna"; + case BiomeKind::Mountains: return "Mountains"; + case BiomeKind::Count: break; + } + return "Unknown"; +} + +BiomeGen::BiomeGen(const BiomeGenParams& biomeParams) + : params(biomeParams), + temperatureNoise(biomeParams.seed ^ 0x9E3779B1u), + humidityNoise(biomeParams.seed ^ 0x85EBCA6Bu), + continentalnessNoise(biomeParams.seed ^ 0xC2B2AE35u), + erosionNoise(biomeParams.seed ^ 0x27D4EB2Fu), + weirdnessNoise(biomeParams.seed ^ 0x165667B1u) {} + +ClimateSample BiomeGen::climateAt(int gx, int gz) const { + FbmParams fbmParams; + fbmParams.octaves = 4; + fbmParams.lacunarity = 2.0f; + fbmParams.gain = 0.5f; + + float cx = static_cast(gx); + float cz = static_cast(gz); + + ClimateSample climate; + climate.temperature = temperatureNoise.fbm2D(cx * params.climateFrequency, cz * params.climateFrequency, fbmParams); + climate.humidity = humidityNoise.fbm2D(cx * params.climateFrequency, cz * params.climateFrequency, fbmParams); + climate.continentalness = continentalnessNoise.fbm2D( + cx * params.climateFrequency * 0.6f, cz * params.climateFrequency * 0.6f, fbmParams); + climate.erosion = erosionNoise.fbm2D(cx * params.erosionFrequency, cz * params.erosionFrequency, fbmParams); + climate.weirdness = weirdnessNoise.fbm2D(cx * params.weirdnessFrequency, cz * params.weirdnessFrequency, fbmParams); + return climate; +} + +BiomeKind BiomeGen::biomeFromClimate(const ClimateSample& climate) const { + if (climate.continentalness < -0.35f) return BiomeKind::Ocean; + if (climate.continentalness < -0.2f) return BiomeKind::Beach; + if (climate.erosion > 0.45f) return BiomeKind::Mountains; + + if (climate.temperature < -0.35f) return BiomeKind::Tundra; + if (climate.temperature < -0.05f) return BiomeKind::Taiga; + + if (climate.temperature > 0.45f) { + if (climate.humidity < -0.1f) return BiomeKind::Desert; + if (climate.humidity > 0.35f) return BiomeKind::Jungle; + return BiomeKind::Savanna; + } + + if (climate.humidity > 0.4f) return BiomeKind::Swamp; + if (climate.humidity > 0.0f) return BiomeKind::Forest; + return BiomeKind::Plains; +} + +BiomeKind BiomeGen::biomeAt(int gx, int gz) const { + return biomeFromClimate(climateAt(gx, gz)); +} + +bool BiomeGen::isRiver(int gx, int gz) const { + ClimateSample climate = climateAt(gx, gz); + if (climate.continentalness < -0.35f) return false; // no rivers carved into open ocean + return std::fabs(climate.weirdness) < params.riverWidth; +} + +SurfaceLayers BiomeGen::surfaceFor(BiomeKind biome) const { + switch (biome) { + case BiomeKind::Ocean: return {Block::Sand, Block::Sand, 4}; + case BiomeKind::Beach: return {Block::Sand, Block::Sand, 3}; + case BiomeKind::Desert: return {Block::Sand, Block::Sandstone, 5}; + case BiomeKind::Plains: return {Block::Grass, Block::Dirt, 3}; + case BiomeKind::Forest: return {Block::Grass, Block::Dirt, 3}; + case BiomeKind::Taiga: return {Block::Grass, Block::Dirt, 3}; + case BiomeKind::Tundra: return {Block::Snow, Block::Dirt, 2}; + case BiomeKind::Swamp: return {Block::Grass, Block::Dirt, 4}; + case BiomeKind::Jungle: return {Block::Grass, Block::Dirt, 3}; + case BiomeKind::Savanna: return {Block::Grass, Block::Dirt, 2}; + case BiomeKind::Mountains: return {Block::Stone, Block::Stone, 1}; + case BiomeKind::Count: break; + } + return {Block::Grass, Block::Dirt, 3}; +} + +Block BiomeGen::surfaceBlockAt(int gx, int gz, int height) const { + if (isRiver(gx, gz) && height <= params.seaLevel + 1) return Block::Water; + + BiomeKind biome = biomeAt(gx, gz); + if (biome == BiomeKind::Mountains && height > params.seaLevel + 46) return Block::Snow; + + return surfaceFor(biome).top; +} + +DecoratorDensity BiomeGen::decoratorsFor(BiomeKind biome) const { + switch (biome) { + case BiomeKind::Ocean: return {0.0f, 0.0f, 0.0f}; + case BiomeKind::Beach: return {0.0f, 0.02f, 0.0f}; + case BiomeKind::Desert: return {0.0f, 0.0f, 0.05f}; + case BiomeKind::Plains: return {0.02f, 0.08f, 0.0f}; + case BiomeKind::Forest: return {0.22f, 0.08f, 0.0f}; + case BiomeKind::Taiga: return {0.20f, 0.04f, 0.0f}; + case BiomeKind::Tundra: return {0.03f, 0.01f, 0.0f}; + case BiomeKind::Swamp: return {0.10f, 0.06f, 0.10f}; + case BiomeKind::Jungle: return {0.35f, 0.12f, 0.02f}; + case BiomeKind::Savanna: return {0.05f, 0.05f, 0.0f}; + case BiomeKind::Mountains: return {0.01f, 0.0f, 0.0f}; + case BiomeKind::Count: break; + } + return {0.0f, 0.0f, 0.0f}; +} + +BiomeWeights BiomeGen::biomeBlend(int gx, int gz, int radius) const { + BiomeWeights weights; + int total = 0; + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + BiomeKind biome = biomeAt(gx + dx, gz + dz); + weights.weight[static_cast(biome)] += 1.0f; + ++total; + } + } + for (float& w : weights.weight) w /= static_cast(total); + return weights; +} + +} // namespace vf