From d0707fde9d52534badb22539494fe49ac6c90c4a Mon Sep 17 00:00:00 2001 From: MDietermann Date: Sun, 31 May 2026 21:50:09 +0200 Subject: [PATCH] feat: add deterministic VoronoiCells partition --- .../WorldGen/VoronoiCellsTests.cs | 36 +++++++++++ World/WorldGen/VoronoiCells.cs | 60 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 VoxelEngine.Tests/WorldGen/VoronoiCellsTests.cs create mode 100644 World/WorldGen/VoronoiCells.cs diff --git a/VoxelEngine.Tests/WorldGen/VoronoiCellsTests.cs b/VoxelEngine.Tests/WorldGen/VoronoiCellsTests.cs new file mode 100644 index 0000000..f1b3708 --- /dev/null +++ b/VoxelEngine.Tests/WorldGen/VoronoiCellsTests.cs @@ -0,0 +1,36 @@ +using VoxelEngine.World.WorldGen; +using Xunit; + +namespace VoxelEngine.Tests.WorldGen; + +public class VoronoiCellsTests +{ + [Fact] + public void Nearest_IsDeterministic() + { + var a = new VoronoiCells(42, 96f); + var b = new VoronoiCells(42, 96f); + Assert.Equal(a.Nearest(123.4f, 56.7f), b.Nearest(123.4f, 56.7f)); + } + + [Fact] + public void Nearest_IsLocallyConstant() + { + var cells = new VoronoiCells(42, 96f); + // Two points a tiny step apart almost always share a cell. + var c1 = cells.Nearest(500f, 500f); + var c2 = cells.Nearest(500.01f, 500.01f); + Assert.Equal(c1.Id, c2.Id); + } + + [Fact] + public void DifferentSeeds_DifferAtSomePoint() + { + var a = new VoronoiCells(1, 96f); + var b = new VoronoiCells(2, 96f); + bool anyDiff = false; + for (int i = 0; i < 50 && !anyDiff; i++) + anyDiff = a.Nearest(i * 31f, i * 17f).Id != b.Nearest(i * 31f, i * 17f).Id; + Assert.True(anyDiff); + } +} diff --git a/World/WorldGen/VoronoiCells.cs b/World/WorldGen/VoronoiCells.cs new file mode 100644 index 0000000..ac5291b --- /dev/null +++ b/World/WorldGen/VoronoiCells.cs @@ -0,0 +1,60 @@ +namespace VoxelEngine.World.WorldGen; + +// A jittered-grid Voronoi partition. Pure integer-hash based, fully deterministic. +public sealed class VoronoiCells +{ + public readonly record struct Cell(int Id, float CenterX, float CenterZ); + + private readonly int _seed; + private readonly float _cellSize; + + public VoronoiCells(int seed, float cellSize) + { + this._seed = seed; + this._cellSize = cellSize; + } + + public Cell Nearest(float x, float z) + { + int gx = (int)System.MathF.Floor(x / this._cellSize); + int gz = (int)System.MathF.Floor(z / this._cellSize); + + float bestDist = float.MaxValue; + int bestId = 0; + float bestCx = 0f, bestCz = 0f; + + for (int oz = -1; oz <= 1; oz++) + for (int ox = -1; ox <= 1; ox++) + { + int cgx = gx + ox; + int cgz = gz + oz; + float jitterX = VoronoiCells.Jitter(cgx, cgz, this._seed); + float jitterZ = VoronoiCells.Jitter(cgx, cgz, this._seed + 9871); + float cx = (cgx + jitterX) * this._cellSize; + float cz = (cgz + jitterZ) * this._cellSize; + float dx = cx - x, dz = cz - z; + float dist = dx * dx + dz * dz; + if (dist < bestDist) + { + bestDist = dist; + bestId = (int)VoronoiCells.Hash(cgx, cgz, this._seed); + bestCx = cx; + bestCz = cz; + } + } + return new Cell(bestId, bestCx, bestCz); + } + + private static float Jitter(int x, int z, int seed) + => (VoronoiCells.Hash(x, z, seed) & 0xFFFFFF) / (float)0x1000000; // [0,1) + + private static uint Hash(int x, int z, int seed) + { + unchecked + { + uint h = (uint)(x * 374761393 + z * 668265263 + seed * 362437); + h = (h ^ (h >> 13)) * 1274126177u; + return h ^ (h >> 16); + } + } +}