b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
175 lines
7.1 KiB
C#
175 lines
7.1 KiB
C#
// 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 ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>Returns noise in approximately [-1, 1].</summary>
|
||
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),
|
||
};
|
||
|
||
/// <summary>Returns noise remapped to [0, 1].</summary>
|
||
public float GetNoise01(float x, float y) => (GetNoise(x, y) + 1f) * 0.5f;
|
||
}
|