diff --git a/VoxelEngine.Tests/WorldGen/BiomeTests.cs b/VoxelEngine.Tests/WorldGen/BiomeTests.cs new file mode 100644 index 0000000..acdfbaa --- /dev/null +++ b/VoxelEngine.Tests/WorldGen/BiomeTests.cs @@ -0,0 +1,46 @@ +using VoxelEngine.World.Blocks; +using VoxelEngine.World.WorldGen; +using Xunit; + +namespace VoxelEngine.Tests.WorldGen; + +public class BiomeTests +{ + [Fact] + public void Desert_TopAndFiller_AreSand() + { + var blocks = new BlockRegistry(); + var biomes = new BiomeRegistry(42, blocks); + Assert.Equal("Sand", biomes.Desert.Surface.Top.Name); + Assert.Equal("Sand", biomes.Desert.Surface.Filler.Name); + } + + [Fact] + public void Mountains_UseSnowAboveSnowLine_StoneBelow() + { + var blocks = new BlockRegistry(); + var biomes = new BiomeRegistry(42, blocks); + Assert.Equal("Stone", biomes.Mountains.TopBlock(70).Name); + Assert.Equal("Snow Block", biomes.Mountains.TopBlock(120).Name); + } + + [Fact] + public void Plains_TopBlock_IsGrass_RegardlessOfHeight() + { + var blocks = new BlockRegistry(); + var biomes = new BiomeRegistry(42, blocks); + Assert.Equal("Grass", biomes.Plains.TopBlock(40).Name); + Assert.Equal("Grass", biomes.Plains.TopBlock(120).Name); + } + + [Fact] + public void SampleHeight_IsDeterministic() + { + var blocks = new BlockRegistry(); + var biomes = new BiomeRegistry(42, blocks); + var noise = new NoiseSource(42); + float h1 = biomes.Mountains.SampleHeight(noise, 100f, 200f); + float h2 = biomes.Mountains.SampleHeight(noise, 100f, 200f); + Assert.Equal(h1, h2); + } +} diff --git a/World/WorldGen/BiomeRegistry.cs b/World/WorldGen/BiomeRegistry.cs new file mode 100644 index 0000000..04d742f --- /dev/null +++ b/World/WorldGen/BiomeRegistry.cs @@ -0,0 +1,20 @@ +using VoxelEngine.World.Blocks; +using VoxelEngine.World.WorldGen.Biomes; + +namespace VoxelEngine.World.WorldGen; + +public sealed class BiomeRegistry +{ + public IBiome Plains { get; } + public IBiome Desert { get; } + public IBiome Mountains { get; } + public IBiome Forest { get; } + + public BiomeRegistry(int seed, BlockRegistry blocks) + { + this.Plains = new PlainsBiome(seed, blocks); + this.Desert = new DesertBiome(seed, blocks); + this.Mountains = new MountainsBiome(seed, blocks); + this.Forest = new ForestBiome(seed, blocks); + } +} diff --git a/World/WorldGen/Biomes/DesertBiome.cs b/World/WorldGen/Biomes/DesertBiome.cs new file mode 100644 index 0000000..9cbc554 --- /dev/null +++ b/World/WorldGen/Biomes/DesertBiome.cs @@ -0,0 +1,25 @@ +using VoxelEngine.World.Blocks; + +namespace VoxelEngine.World.WorldGen.Biomes; + +public sealed class DesertBiome : IBiome +{ + private readonly FastNoiseLite _detail; + + public string Name => "Desert"; + public SurfacePalette Surface { get; } + + public DesertBiome(int seed, BlockRegistry blocks) + { + this._detail = new FastNoiseLite(seed + 101); + this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); + this._detail.SetFrequency(0.015f); + this.Surface = new SurfacePalette(blocks.Sand, blocks.Sand, blocks.Stone); + } + + public float SampleHeight(NoiseSource noise, float worldX, float worldZ) + => WorldConstants.SeaLevel + 4f * noise.Continent(worldX, worldZ) + + 1.5f * this._detail.GetNoise(worldX, worldZ); + + public IBlock TopBlock(int worldY) => this.Surface.Top; +} diff --git a/World/WorldGen/Biomes/ForestBiome.cs b/World/WorldGen/Biomes/ForestBiome.cs new file mode 100644 index 0000000..4dffead --- /dev/null +++ b/World/WorldGen/Biomes/ForestBiome.cs @@ -0,0 +1,25 @@ +using VoxelEngine.World.Blocks; + +namespace VoxelEngine.World.WorldGen.Biomes; + +public sealed class ForestBiome : IBiome +{ + private readonly FastNoiseLite _detail; + + public string Name => "Forest"; + public SurfacePalette Surface { get; } + + public ForestBiome(int seed, BlockRegistry blocks) + { + this._detail = new FastNoiseLite(seed + 103); + this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); + this._detail.SetFrequency(0.03f); + this.Surface = new SurfacePalette(blocks.Grass, blocks.Dirt, blocks.Stone); + } + + public float SampleHeight(NoiseSource noise, float worldX, float worldZ) + => WorldConstants.SeaLevel + 6f * noise.Continent(worldX, worldZ) + + 4f * this._detail.GetNoise(worldX, worldZ); + + public IBlock TopBlock(int worldY) => this.Surface.Top; +} diff --git a/World/WorldGen/Biomes/MountainsBiome.cs b/World/WorldGen/Biomes/MountainsBiome.cs new file mode 100644 index 0000000..2737f94 --- /dev/null +++ b/World/WorldGen/Biomes/MountainsBiome.cs @@ -0,0 +1,32 @@ +using VoxelEngine.World.Blocks; + +namespace VoxelEngine.World.WorldGen.Biomes; + +public sealed class MountainsBiome : IBiome +{ + private const int SnowLine = 96; + private readonly FastNoiseLite _ridged; + private readonly IBlock _stone; + private readonly IBlock _snow; + + public string Name => "Mountains"; + public SurfacePalette Surface { get; } + + public MountainsBiome(int seed, BlockRegistry blocks) + { + this._ridged = new FastNoiseLite(seed + 102); + this._ridged.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); + this._ridged.SetFractalType(FastNoiseLite.FractalType.Ridged); + this._ridged.SetFractalOctaves(4); + this._ridged.SetFrequency(0.01f); + this._stone = blocks.Stone; + this._snow = blocks.Snow; + this.Surface = new SurfacePalette(blocks.Stone, blocks.Stone, blocks.Stone); + } + + public float SampleHeight(NoiseSource noise, float worldX, float worldZ) + => WorldConstants.SeaLevel + 10f * noise.Continent(worldX, worldZ) + + 45f * this._ridged.GetNoise(worldX, worldZ); + + public IBlock TopBlock(int worldY) => worldY >= MountainsBiome.SnowLine ? this._snow : this._stone; +} diff --git a/World/WorldGen/Biomes/PlainsBiome.cs b/World/WorldGen/Biomes/PlainsBiome.cs new file mode 100644 index 0000000..7ec1731 --- /dev/null +++ b/World/WorldGen/Biomes/PlainsBiome.cs @@ -0,0 +1,25 @@ +using VoxelEngine.World.Blocks; + +namespace VoxelEngine.World.WorldGen.Biomes; + +public sealed class PlainsBiome : IBiome +{ + private readonly FastNoiseLite _detail; + + public string Name => "Plains"; + public SurfacePalette Surface { get; } + + public PlainsBiome(int seed, BlockRegistry blocks) + { + this._detail = new FastNoiseLite(seed + 100); + this._detail.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); + this._detail.SetFrequency(0.02f); + this.Surface = new SurfacePalette(blocks.Grass, blocks.Dirt, blocks.Stone); + } + + public float SampleHeight(NoiseSource noise, float worldX, float worldZ) + => WorldConstants.SeaLevel + 6f * noise.Continent(worldX, worldZ) + + 2f * this._detail.GetNoise(worldX, worldZ); + + public IBlock TopBlock(int worldY) => this.Surface.Top; +}