// FastNoiseLite — vendored C# implementation.
// Based on the FastNoiseLite library by Jordan Peck (Auburn).
// Provides 2D Simplex noise with fractal FBm support.
namespace Theriapolis.Core.Util;
public sealed class FastNoiseLite
{
public enum NoiseType { OpenSimplex2, Simplex, Perlin }
public enum FractalType { None, FBm, Ridged, PingPong }
// ── Properties ────────────────────────────────────────────────────────────
public int Seed { get; set; } = 1337;
public float Frequency { get; set; } = 0.01f;
public NoiseType Noise { get; set; } = NoiseType.OpenSimplex2;
public FractalType Fractal { get; set; } = FractalType.FBm;
public int Octaves { get; set; } = 3;
public float Lacunarity{ get; set; } = 2.0f;
public float Gain { get; set; } = 0.5f;
public float WeightedStrength { get; set; } = 0.0f;
// ── Permutation table ─────────────────────────────────────────────────────
private readonly int[] _perm = new int[512];
private int _cachedSeed = int.MinValue;
private void EnsurePerm()
{
if (_cachedSeed == Seed) return;
_cachedSeed = Seed;
// Build the shuffled permutation table from the seed
var p = new int[256];
for (int i = 0; i < 256; i++) p[i] = i;
// Seeded Fisher–Yates
ulong state = (ulong)Seed ^ 0x9e3779b97f4a7c15UL;
for (int i = 255; i > 0; i--)
{
state += 0x9e3779b97f4a7c15UL;
ulong z = state;
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9UL;
z = (z ^ (z >> 27)) * 0x94d049bb133111ebUL;
int j = (int)((z ^ (z >> 31)) % (ulong)(i + 1));
(p[i], p[j]) = (p[j], p[i]);
}
for (int i = 0; i < 512; i++) _perm[i] = p[i & 255];
}
// ── 2D gradient table (12 directions) ────────────────────────────────────
private static readonly float[] GradX = { 1, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0, 0 };
private static readonly float[] GradY = { 1, 1, -1, -1, 0, 0, 0, 0, 1, -1, 1, -1 };
private static int FastFloor(float x) => x >= 0 ? (int)x : (int)x - 1;
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
// ── Core 2D simplex noise ────────────────────────────────────────────────
private float Simplex2D(float x, float y)
{
EnsurePerm();
const float F2 = 0.3660254037844387f; // (sqrt(3)-1)/2
const float G2 = 0.21132486540518713f; // (3-sqrt(3))/6
float s = (x + y) * F2;
int i = FastFloor(x + s);
int j = FastFloor(y + s);
float t = (i + j) * G2;
float x0 = x - (i - t);
float y0 = y - (j - t);
int i1, j1;
if (x0 > y0) { i1 = 1; j1 = 0; }
else { i1 = 0; j1 = 1; }
float x1 = x0 - i1 + G2;
float y1 = y0 - j1 + G2;
float x2 = x0 - 1f + 2f * G2;
float y2 = y0 - 1f + 2f * G2;
int gi0 = _perm[( i + _perm[ j & 255]) & 255] % 12;
int gi1 = _perm[((i + i1) + _perm[((j + j1)) & 255]) & 255] % 12;
int gi2 = _perm[( i + 1 + _perm[( j + 1 ) & 255]) & 255] % 12;
float n = Contribution(gi0, x0, y0)
+ Contribution(gi1, x1, y1)
+ Contribution(gi2, x2, y2);
return 70f * n; // scale to approximately [-1, 1]
}
private static float Contribution(int gi, float x, float y)
{
float t = 0.5f - x * x - y * y;
if (t < 0) return 0f;
t *= t;
return t * t * (GradX[gi] * x + GradY[gi] * y);
}
// ── OpenSimplex2 (alias to Simplex for vendored build) ────────────────────
// Full OpenSimplex2 derivation is identical in output characteristics for
// our use; the distinction matters only for tiling, which we don't use.
private float OpenSimplex2_2D(float x, float y)
{
const float sqrt3 = 1.7320508075688772f;
const float F2 = 0.5f * (sqrt3 - 1f);
float t = (x + y) * F2;
x += t; y += t;
return Simplex2D(x, y);
}
// ── Single noise sample (no fractal) ──────────────────────────────────────
private float SingleNoise(float x, float y) => Noise switch
{
NoiseType.OpenSimplex2 => OpenSimplex2_2D(x, y),
NoiseType.Simplex => Simplex2D(x, y),
NoiseType.Perlin => Simplex2D(x, y), // use simplex as stand-in
_ => Simplex2D(x, y),
};
// ── Fractal FBm ───────────────────────────────────────────────────────────
private float FractalFBm(float x, float y)
{
float sum = 0;
float amp = CalcFractalBounding();
float freq = Frequency;
for (int i = 0; i < Octaves; i++)
{
float n = SingleNoise(x * freq, y * freq);
sum += n * amp;
amp *= Lerp(1f, MathF.Min(n + 1f, 2f) * 0.5f, WeightedStrength);
amp *= Gain;
freq *= Lacunarity;
}
return sum;
}
private float FractalRidged(float x, float y)
{
float sum = 0;
float amp = CalcFractalBounding();
float freq = Frequency;
for (int i = 0; i < Octaves; i++)
{
float n = MathF.Abs(SingleNoise(x * freq, y * freq));
sum += (n * -2f + 1f) * amp;
amp *= Lerp(1f, 1f - n, WeightedStrength);
amp *= Gain;
freq *= Lacunarity;
}
return sum;
}
private float CalcFractalBounding()
{
float amp = Gain;
float ampFractal = 1f;
for (int i = 1; i < Octaves; i++)
{
ampFractal += amp;
amp *= Gain;
}
return 1f / ampFractal;
}
// ── Public API ────────────────────────────────────────────────────────────
/// Returns noise in approximately [-1, 1].
public float GetNoise(float x, float y) => Fractal switch
{
FractalType.None => SingleNoise(x * Frequency, y * Frequency),
FractalType.FBm => FractalFBm(x, y),
FractalType.Ridged => FractalRidged(x, y),
FractalType.PingPong => FractalFBm(x, y), // simplification
_ => FractalFBm(x, y),
};
/// Returns noise remapped to [0, 1].
public float GetNoise01(float x, float y) => (GetNoise(x, y) + 1f) * 0.5f;
}