diff --git a/VoxelEngine.Tests/WorldGen/WorldGeneratorTests.cs b/VoxelEngine.Tests/WorldGen/WorldGeneratorTests.cs new file mode 100644 index 0000000..d1aa186 --- /dev/null +++ b/VoxelEngine.Tests/WorldGen/WorldGeneratorTests.cs @@ -0,0 +1,70 @@ +using OpenTK.Mathematics; +using VoxelEngine.World.Blocks; +using VoxelEngine.World.Chunk; +using VoxelEngine.World.WorldGen; +using Xunit; + +namespace VoxelEngine.Tests.WorldGen; + +public class WorldGeneratorTests +{ + private static BlockRegistry Blocks => new(); + + [Fact] + public void BlockAt_AboveTerrainAndSea_IsAir() + { + var blocks = Blocks; + var biomes = new BiomeRegistry(1, blocks); + ushort id = WorldGenerator.BlockAt(80, surfaceHeight: 70, biomes.Plains, blocks); + Assert.Equal(blocks.Air.Id, id); // 80 > 70 and 80 > SeaLevel(64) + } + + [Fact] + public void BlockAt_AboveTerrainBelowSea_IsWater() + { + var blocks = Blocks; + var biomes = new BiomeRegistry(1, blocks); + ushort id = WorldGenerator.BlockAt(60, surfaceHeight: 50, biomes.Plains, blocks); + Assert.Equal(blocks.Water.Id, id); // 60 > 50 but 60 <= SeaLevel(64) + } + + [Fact] + public void BlockAt_Surface_IsBiomeTop() + { + var blocks = Blocks; + var biomes = new BiomeRegistry(1, blocks); + ushort id = WorldGenerator.BlockAt(70, surfaceHeight: 70, biomes.Desert, blocks); + Assert.Equal(blocks.Sand.Id, id); + } + + [Fact] + public void BlockAt_FillerBand_IsBiomeFiller() + { + var blocks = Blocks; + var biomes = new BiomeRegistry(1, blocks); + ushort id = WorldGenerator.BlockAt(68, surfaceHeight: 70, biomes.Plains, blocks); + Assert.Equal(blocks.Dirt.Id, id); // within 3 of surface + } + + [Fact] + public void BlockAt_Deep_IsStone() + { + var blocks = Blocks; + var biomes = new BiomeRegistry(1, blocks); + ushort id = WorldGenerator.BlockAt(40, surfaceHeight: 70, biomes.Plains, blocks); + Assert.Equal(blocks.Stone.Id, id); + } + + [Fact] + public void Generate_IsDeterministic() + { + var gen1 = new WorldGenerator(99, Blocks); + var gen2 = new WorldGenerator(99, Blocks); + Chunk a = gen1.Generate(new Vector3i(0, 4, 0)); + Chunk b = gen2.Generate(new Vector3i(0, 4, 0)); + for (int x = 0; x < Chunk.Size; x++) + for (int y = 0; y < Chunk.Size; y++) + for (int z = 0; z < Chunk.Size; z++) + Assert.Equal(a[x, y, z], b[x, y, z]); + } +} diff --git a/World/WorldGen/WorldGenerator.cs b/World/WorldGen/WorldGenerator.cs new file mode 100644 index 0000000..1e45c45 --- /dev/null +++ b/World/WorldGen/WorldGenerator.cs @@ -0,0 +1,61 @@ +using OpenTK.Mathematics; +using VoxelEngine.World.Blocks; +using VoxelChunk = VoxelEngine.World.Chunk.Chunk; + +namespace VoxelEngine.World.WorldGen; + +public sealed class WorldGenerator +{ + private const int FillerDepth = 3; + private const float CellSize = 96f; + + private readonly BlockRegistry _blocks; + private readonly BiomeMap _biomeMap; + private readonly TerrainSampler _terrain; + + public WorldGenerator(int seed, BlockRegistry blocks) + { + this._blocks = blocks; + NoiseSource noise = new(seed); + VoronoiCells cells = new(seed, WorldGenerator.CellSize); + BiomeRegistry biomes = new(seed, blocks); + this._biomeMap = new BiomeMap(noise, cells, biomes); + this._terrain = new TerrainSampler(noise, this._biomeMap); + } + + public VoxelChunk Generate(Vector3i chunkPos) + { + VoxelChunk chunk = new(); + int baseX = chunkPos.X * VoxelChunk.Size; + int baseY = chunkPos.Y * VoxelChunk.Size; + int baseZ = chunkPos.Z * VoxelChunk.Size; + + for (int x = 0; x < VoxelChunk.Size; x++) + for (int z = 0; z < VoxelChunk.Size; z++) + { + int worldX = baseX + x; + int worldZ = baseZ + z; + int height = this._terrain.HeightAt(worldX, worldZ); + IBiome biome = this._biomeMap.BiomeAt(worldX, worldZ); + + for (int y = 0; y < VoxelChunk.Size; y++) + { + int worldY = baseY + y; + chunk[x, y, z] = WorldGenerator.BlockAt(worldY, height, biome, this._blocks); + } + } + return chunk; + } + + // Pure column fill rule, extracted for testability. + public static ushort BlockAt(int worldY, int surfaceHeight, IBiome biome, BlockRegistry blocks) + { + if (worldY > surfaceHeight) + return worldY <= WorldConstants.SeaLevel ? blocks.Water.Id : blocks.Air.Id; + if (worldY == surfaceHeight) + return biome.TopBlock(worldY).Id; + if (worldY >= surfaceHeight - WorldGenerator.FillerDepth) + return biome.Surface.Filler.Id; + return biome.Surface.Under.Id; + } +}