From 6196b38cc84c0214969b7c632a37d9a870c93dab Mon Sep 17 00:00:00 2001 From: MDietermann Date: Sun, 31 May 2026 22:06:30 +0200 Subject: [PATCH] feat: add TerrainSampler with dense box-blur border smoothing --- .../WorldGen/TerrainSamplerTests.cs | 53 +++++++++++++++++++ World/WorldGen/TerrainSampler.cs | 38 +++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 VoxelEngine.Tests/WorldGen/TerrainSamplerTests.cs create mode 100644 World/WorldGen/TerrainSampler.cs diff --git a/VoxelEngine.Tests/WorldGen/TerrainSamplerTests.cs b/VoxelEngine.Tests/WorldGen/TerrainSamplerTests.cs new file mode 100644 index 0000000..20beeea --- /dev/null +++ b/VoxelEngine.Tests/WorldGen/TerrainSamplerTests.cs @@ -0,0 +1,53 @@ +using VoxelEngine.World.Blocks; +using VoxelEngine.World.WorldGen; +using Xunit; + +namespace VoxelEngine.Tests.WorldGen; + +public class TerrainSamplerTests +{ + private static TerrainSampler 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); + var map = new BiomeMap(noise, cells, biomes); + return new TerrainSampler(noise, map); + } + + [Fact] + public void Height_StaysWithinWorldBounds() + { + var sampler = Build(); + for (int x = -300; x < 300; x += 7) + for (int z = -300; z < 300; z += 7) + { + int h = sampler.HeightAt(x, z); + Assert.InRange(h, 0, WorldConstants.WorldHeight - 1); + } + } + + [Fact] + public void Height_IsDeterministic() + { + var a = Build(); + var b = Build(); + Assert.Equal(a.HeightAt(120, 240), b.HeightAt(120, 240)); + } + + [Fact] + public void AdjacentColumns_DoNotCliff() + { + var sampler = Build(); + for (int x = -200; x < 200; x += 3) + for (int z = -200; z < 200; z += 3) + { + // The dense blur removes inter-biome cliffs (those would be 25+ blocks). + // Remaining deltas are rugged intra-Mountains ridged slopes (~9), which + // are intended terrain, not cliffs; 16 separates the two. + int dx = System.Math.Abs(sampler.HeightAt(x, z) - sampler.HeightAt(x + 1, z)); + Assert.True(dx <= 16, $"height jump {dx} at ({x},{z})"); + } + } +} diff --git a/World/WorldGen/TerrainSampler.cs b/World/WorldGen/TerrainSampler.cs new file mode 100644 index 0000000..6f01a82 --- /dev/null +++ b/World/WorldGen/TerrainSampler.cs @@ -0,0 +1,38 @@ +namespace VoxelEngine.World.WorldGen; + +public sealed class TerrainSampler +{ + // Box-blur radius (in blocks) applied to the biome-height field to smooth + // biome borders. A full (2R+1)x(2R+1) kernel at 1-block spacing bounds the + // per-block height change to ~maxBiomeHeightDiff / (2R+1), preventing cliffs. + // A sparse kernel does NOT bound it (axis-aligned borders flip several samples + // at once), so the kernel must be dense. + private const int BlendRadius = 6; + + private readonly NoiseSource _noise; + private readonly BiomeMap _biomes; + + public TerrainSampler(NoiseSource noise, BiomeMap biomes) + { + this._noise = noise; + this._biomes = biomes; + } + + public int HeightAt(float worldX, float worldZ) + { + // For each kernel point find the biome THERE, but evaluate its height AT the + // column. Averaging blurs the hard biome borders into smooth transitions. + float sum = 0f; + int count = 0; + for (int dz = -TerrainSampler.BlendRadius; dz <= TerrainSampler.BlendRadius; dz++) + for (int dx = -TerrainSampler.BlendRadius; dx <= TerrainSampler.BlendRadius; dx++) + { + IBiome biome = this._biomes.BiomeAt(worldX + dx, worldZ + dz); + sum += biome.SampleHeight(this._noise, worldX, worldZ); + count++; + } + float avg = sum / count; + int h = (int)System.MathF.Round(avg); + return System.Math.Clamp(h, 0, WorldConstants.WorldHeight - 1); + } +}