diff --git a/VoxelEngine.Tests/WorldGen/BiomeMapTests.cs b/VoxelEngine.Tests/WorldGen/BiomeMapTests.cs new file mode 100644 index 0000000..7cb86b6 --- /dev/null +++ b/VoxelEngine.Tests/WorldGen/BiomeMapTests.cs @@ -0,0 +1,54 @@ +using VoxelEngine.World.Blocks; +using VoxelEngine.World.WorldGen; +using Xunit; + +namespace VoxelEngine.Tests.WorldGen; + +public class BiomeMapTests +{ + private static (BiomeMap map, BiomeRegistry biomes) Build(int seed = 42) + { + var blocks = new BlockRegistry(); + var biomes = new BiomeRegistry(seed, blocks); + var noise = new NoiseSource(seed); + var cells = new VoronoiCells(seed, 96f); + return (new BiomeMap(noise, cells, biomes), biomes); + } + + [Fact] + public void Classify_HighElevation_IsMountains() + { + var (_, biomes) = Build(); + Assert.Same(biomes.Mountains, BiomeMap.Classify(0f, 0.5f, 0f, biomes)); + } + + [Fact] + public void Classify_HotLowElevation_IsDesert() + { + var (_, biomes) = Build(); + Assert.Same(biomes.Desert, BiomeMap.Classify(0.5f, 0f, 0f, biomes)); + } + + [Fact] + public void Classify_TemperateWooded_IsForest() + { + var (_, biomes) = Build(); + Assert.Same(biomes.Forest, BiomeMap.Classify(0f, 0f, 0.5f, biomes)); + } + + [Fact] + public void Classify_ColdLow_IsPlains() + { + var (_, biomes) = Build(); + Assert.Same(biomes.Plains, BiomeMap.Classify(-0.9f, 0f, 0f, biomes)); + } + + [Fact] + public void BiomeAt_IsConstantWithinACell() + { + var (map, _) = Build(); + IBiome a = map.BiomeAt(500f, 500f); + IBiome b = map.BiomeAt(500.01f, 500.01f); // same cell + Assert.Same(a, b); + } +} diff --git a/World/WorldGen/BiomeMap.cs b/World/WorldGen/BiomeMap.cs new file mode 100644 index 0000000..857a5c6 --- /dev/null +++ b/World/WorldGen/BiomeMap.cs @@ -0,0 +1,36 @@ +namespace VoxelEngine.World.WorldGen; + +public sealed class BiomeMap +{ + private readonly NoiseSource _noise; + private readonly VoronoiCells _cells; + private readonly BiomeRegistry _biomes; + + public BiomeMap(NoiseSource noise, VoronoiCells cells, BiomeRegistry biomes) + { + this._noise = noise; + this._cells = cells; + this._biomes = biomes; + } + + public int CellId(float worldX, float worldZ) => this._cells.Nearest(worldX, worldZ).Id; + + public IBiome BiomeAt(float worldX, float worldZ) + { + VoronoiCells.Cell cell = this._cells.Nearest(worldX, worldZ); + // Sample climate at the cell center so the biome is constant across the cell. + float temperature = this._noise.Temperature(cell.CenterX, cell.CenterZ); + float elevation = this._noise.Continent(cell.CenterX, cell.CenterZ); + float forest = this._noise.Forest(cell.CenterX, cell.CenterZ); + return BiomeMap.Classify(temperature, elevation, forest, this._biomes); + } + + // Pure climate -> biome lookup. Thresholds in noise units (~[-1,1]). + public static IBiome Classify(float temperature, float elevation, float forest, BiomeRegistry biomes) + { + if (elevation > 0.25f) return biomes.Mountains; // high ground + if (temperature > 0.33f) return biomes.Desert; // hot, low + if (temperature > -0.33f && forest > 0f) return biomes.Forest; // temperate, wooded + return biomes.Plains; // temperate/cold, low + } +}