feat: add deterministic VoronoiCells partition
This commit is contained in:
36
VoxelEngine.Tests/WorldGen/VoronoiCellsTests.cs
Normal file
36
VoxelEngine.Tests/WorldGen/VoronoiCellsTests.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
World/WorldGen/VoronoiCells.cs
Normal file
60
World/WorldGen/VoronoiCells.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user